如何将 UITableViewController 显示为另一个 ViewController 前面的向上滑动手势?
How to display UITableViewController as a swipe up gesture in front of another ViewController?
我正在努力让这样的东西发挥作用。这是优步应用程序。用户可以在背景视图前面向上滑动另一个视图。
背景视图比较简单,已经做好了。将在顶部滑动的视图将是一个 UITableView。我希望用户能够在应用程序启动时首先看到顶部的一点点,然后滑动一点它应该停在中间,然后在完全向上滑动后应该将它一直带到顶部,替换背景查看。
我看过的框架是 iOS 的可拉取视图。但它太旧了,没有任何好的动画。我也看过 SWRevealViewController,但我不知道如何从下方向上滑动。
我也尝试过使用一个按钮,这样当用户点击它时,table 视图控制器以模态方式出现,覆盖垂直方向,但这不是我想要的。它需要识别手势。
非常感谢任何帮助。谢谢
您可以将 table 视图嵌入自定义滚动视图中,该视图仅在触摸 table 视图部分(覆盖 hittest)时处理触摸,然后将其向上拖动(禁用 tableview scroll),直到上部然后禁用滚动视图并再次启用tableview scroll
或者,您可以只将滑动手势添加到您的 table 视图中并更改它的框架并在它到达顶部时禁用滑动
试验这些,最终你会达到你想要的效果
正如 Tj3n 所指出的,您可以使用 UISwipeGesture
来显示 UITableView
。所以使用约束(而不是框架)继承人如何去做:
转到您想要显示 UITableView
的故事板中的 UIViewController
。拖放 UITableView
并向 UITableView
添加前导、尾随和高度。现在在 UIViewController
和 UITableView
之间添加一个垂直约束,以便 UITableView
出现在 下方 UIViewController
(玩这个垂直值,直到您可以显示 UITableView
的顶部以满足您的需要)。为垂直间距约束和高度约束创建出口(以防您需要设置可以在 运行 时间计算出的特定高度)。在向上滑动时,将垂直约束动画设置为等于高度的负值,例如:
topSpaceToViewControllerConstraint.constant = -mainTableViewHeightConstraint.constant
UIView.animate(withDuration: 0.3) {
view.layoutIfNeeded()
};
或者
如果您希望能够根据平移量(即取决于用户在屏幕上移动的距离或速度)调高 UITableView
,您应该使用 UIPanGestureRecognizer
而不是尝试为 UITableView
设置框架而不是 autoLayout (因为我不是反复调用 view.layoutIfNeeded
的忠实粉丝。我在某处读到这是一项昂贵的操作,如果有人会确认或更正这一点)。
func handlePan(sender: UIPanGestureRecognizer) {
if sender.state == .Changed {
//update y origin value here based on the pan amount
}
}
或者使用 UITableViewController
如果您愿意,也可以使用 UITableViewController
执行您希望执行的操作,但是通过创建自定义 UINavigationControllerDelegate
主要是为了创建自定义动画需要大量伪造和努力如果需要,将使用 UIPercentDrivenInteractiveTransition
使用 UIPanGestureRecognizer
拉起新的 UITableViewController
,具体取决于平移量。否则你可以简单地添加一个 UISwipeGestureRecognizer
来呈现 UITableViewController
但你仍然需要再次创建一个自定义动画来 "fake" 你想要的效果。
编辑: 所以,一段时间过去了,现在有一个非常棒的库,叫做 Pulley。它完全符合我的要求,而且设置起来轻而易举!
原回答:
感谢 Rikh 和 Tj3n 给我的提示。我设法做了一些非常基本的事情,它没有像优步那样漂亮的动画,但它完成了工作。
使用下面的代码,你可以滑动任何UIViewController。我在图像上使用了 UIPanGestureRecognizer,它将始终位于拖动视图的顶部。基本上,您使用该图像,它会识别它被拖动的位置,并根据用户的输入设置视图的框架。
首先转到您的故事板并为将要拖动的 UIViewController 添加标识符。
然后在MainViewController中,使用如下代码:
class MainViewController: UIViewController {
// This image will be dragged up or down.
@IBOutlet var imageView: UIImageView!
// Gesture recognizer, will be added to image below.
var swipedOnImage = UIPanGestureRecognizer()
// This is the view controller that will be dragged with the image. In my case it's a UITableViewController.
var vc = UIViewController()
override func viewDidLoad() {
super.viewDidLoad()
// I'm using a storyboard.
let sb = UIStoryboard(name: "Main", bundle: nil)
// I have identified the view inside my storyboard.
vc = sb.instantiateViewController(withIdentifier: "TableVC")
// These values can be played around with, depending on how much you want the view to show up when it starts.
vc.view.frame = CGRect(x: 0, y: self.view.frame.height, width: self.view.frame.width, height: -300)
self.addChildViewController(vc)
self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
swipedOnImage = UIPanGestureRecognizer(target: self, action: #selector(self.swipedOnViewAction))
imageView.addGestureRecognizer(swipedOnImage)
imageView.isUserInteractionEnabled = true
}
// This function handles resizing of the tableview.
func swipedOnViewAction() {
let yLocationTouched = swipedOnImage.location(in: self.view).y
imageView.frame.origin.y = yLocationTouched
// These values can be played around with if required.
vc.view.frame = CGRect(x: 0, y: yLocationTouched, width: UIScreen.main.bounds.width, height: (UIScreen.main.bounds.height) - (yLocationTouched))
vc.view.frame.origin.y = yLocationTouched + 50
}
最终产品
现在,我的回答可能不是最有效的方法,但我是 iOS 的新手,所以这是我目前能想到的最好方法。
我知道这个问题已经有将近 2 年半的历史了,但以防万一有人通过搜索引擎找到这个问题:
我会说你最好的选择是使用 UIViewPropertyAnimator
。这里有一篇很棒的文章:http://www.swiftkickmobile.com/building-better-app-animations-swift-uiviewpropertyanimator/
编辑:
我设法得到一个使用 UIViewPropertyAnimator
的简单原型,这是它的 GIF:
这是 Github 上的项目:https://github.com/Luigi123/UIViewPropertyAnimatorExample
基本上我有两个UIViewController
,主要的ViewController
和次要的BottomSheetViewController
。辅助视图有一个 UIPanGestureRecognizer
使其可拖动,在识别器的回调函数中我做了 3 件事(在实际移动它之后):
①计算屏幕被拖动的百分比,
② 在辅助视图中触发动画,
③ 通知主视图有关拖动操作,以便它可以触发它的动画。在这种情况下,我使用 Notification
,将百分比传递给 notification.userInfo
.
我不确定如何表达①,举个例子,如果屏幕是 500 像素高,用户将次视图拖动到第 100 个像素,我计算出用户将它拖动了 20%一路攀升。这个百分比正是我需要传递到 UIViewPropertyAnimator
个实例中的 fractionComplete
属性 的百分比。
⚠️ 需要注意的一件事是,我无法让它与实际的导航栏一起工作,所以我使用了一个 "normal" 视图,并在其中放置了一个标签。
我尝试通过删除一些实用功能(例如检查用户交互是否完成)来使代码更小,但这意味着用户可以停止在屏幕中间拖动并且应用程序根本不会做出反应,所以我真的建议您在 github 存储库中查看完整代码。但好消息是,执行动画的整个代码大约有 100 行代码。
考虑到这一点,这是主屏幕的代码,ViewController
:
import UIKit
import MapKit
import NotificationCenter
class ViewController: UIViewController {
@IBOutlet weak var someView: UIView!
@IBOutlet weak var blackView: UIView!
var animator: UIViewPropertyAnimator?
func createBottomView() {
guard let sub = storyboard!.instantiateViewController(withIdentifier: "BottomSheetViewController") as? BottomSheetViewController else { return }
self.addChild(sub)
self.view.addSubview(sub.view)
sub.didMove(toParent: self)
sub.view.frame = CGRect(x: 0, y: view.frame.maxY - 100, width: view.frame.width, height: view.frame.height)
}
func subViewGotPanned(_ percentage: Int) {
guard let propAnimator = animator else {
animator = UIViewPropertyAnimator(duration: 3, curve: .linear, animations: {
self.blackView.alpha = 1
self.someView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8).concatenating(CGAffineTransform(translationX: 0, y: -20))
})
animator?.startAnimation()
animator?.pauseAnimation()
return
}
propAnimator.fractionComplete = CGFloat(percentage) / 100
}
func receiveNotification(_ notification: Notification) {
guard let percentage = notification.userInfo?["percentage"] as? Int else { return }
subViewGotPanned(percentage)
}
override func viewDidLoad() {
super.viewDidLoad()
createBottomView()
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil, using: receiveNotification(_:))
}
}
以及辅助视图的代码 (BottomSheetViewController
):
import UIKit
import NotificationCenter
class BottomSheetViewController: UIViewController, UIGestureRecognizerDelegate {
@IBOutlet weak var navBarView: UIView!
var panGestureRecognizer: UIPanGestureRecognizer?
var animator: UIViewPropertyAnimator?
override func viewDidLoad() {
gotPanned(0)
super.viewDidLoad()
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(respondToPanGesture))
view.addGestureRecognizer(gestureRecognizer)
gestureRecognizer.delegate = self
panGestureRecognizer = gestureRecognizer
}
func gotPanned(_ percentage: Int) {
if animator == nil {
animator = UIViewPropertyAnimator(duration: 1, curve: .linear, animations: {
let scaleTransform = CGAffineTransform(scaleX: 1, y: 5).concatenating(CGAffineTransform(translationX: 0, y: 240))
self.navBarView.transform = scaleTransform
self.navBarView.alpha = 0
})
animator?.isReversed = true
animator?.startAnimation()
animator?.pauseAnimation()
}
animator?.fractionComplete = CGFloat(percentage) / 100
}
// MARK: methods to make the view draggable
@objc func respondToPanGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
moveToY(self.view.frame.minY + translation.y)
recognizer.setTranslation(.zero, in: self.view)
}
private func moveToY(_ position: CGFloat) {
view.frame = CGRect(x: 0, y: position, width: view.frame.width, height: view.frame.height)
let maxHeight = view.frame.height - 100
let percentage = Int(100 - ((position * 100) / maxHeight))
gotPanned(percentage)
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.post(name: name, object: nil, userInfo: ["percentage": percentage])
}
}
我正在努力让这样的东西发挥作用。这是优步应用程序。用户可以在背景视图前面向上滑动另一个视图。
背景视图比较简单,已经做好了。将在顶部滑动的视图将是一个 UITableView。我希望用户能够在应用程序启动时首先看到顶部的一点点,然后滑动一点它应该停在中间,然后在完全向上滑动后应该将它一直带到顶部,替换背景查看。
我看过的框架是 iOS 的可拉取视图。但它太旧了,没有任何好的动画。我也看过 SWRevealViewController,但我不知道如何从下方向上滑动。
我也尝试过使用一个按钮,这样当用户点击它时,table 视图控制器以模态方式出现,覆盖垂直方向,但这不是我想要的。它需要识别手势。
非常感谢任何帮助。谢谢
您可以将 table 视图嵌入自定义滚动视图中,该视图仅在触摸 table 视图部分(覆盖 hittest)时处理触摸,然后将其向上拖动(禁用 tableview scroll),直到上部然后禁用滚动视图并再次启用tableview scroll
或者,您可以只将滑动手势添加到您的 table 视图中并更改它的框架并在它到达顶部时禁用滑动
试验这些,最终你会达到你想要的效果
正如 Tj3n 所指出的,您可以使用 UISwipeGesture
来显示 UITableView
。所以使用约束(而不是框架)继承人如何去做:
转到您想要显示 UITableView
的故事板中的 UIViewController
。拖放 UITableView
并向 UITableView
添加前导、尾随和高度。现在在 UIViewController
和 UITableView
之间添加一个垂直约束,以便 UITableView
出现在 下方 UIViewController
(玩这个垂直值,直到您可以显示 UITableView
的顶部以满足您的需要)。为垂直间距约束和高度约束创建出口(以防您需要设置可以在 运行 时间计算出的特定高度)。在向上滑动时,将垂直约束动画设置为等于高度的负值,例如:
topSpaceToViewControllerConstraint.constant = -mainTableViewHeightConstraint.constant
UIView.animate(withDuration: 0.3) {
view.layoutIfNeeded()
};
或者
如果您希望能够根据平移量(即取决于用户在屏幕上移动的距离或速度)调高 UITableView
,您应该使用 UIPanGestureRecognizer
而不是尝试为 UITableView
设置框架而不是 autoLayout (因为我不是反复调用 view.layoutIfNeeded
的忠实粉丝。我在某处读到这是一项昂贵的操作,如果有人会确认或更正这一点)。
func handlePan(sender: UIPanGestureRecognizer) {
if sender.state == .Changed {
//update y origin value here based on the pan amount
}
}
或者使用 UITableViewController
如果您愿意,也可以使用 UITableViewController
执行您希望执行的操作,但是通过创建自定义 UINavigationControllerDelegate
主要是为了创建自定义动画需要大量伪造和努力如果需要,将使用 UIPercentDrivenInteractiveTransition
使用 UIPanGestureRecognizer
拉起新的 UITableViewController
,具体取决于平移量。否则你可以简单地添加一个 UISwipeGestureRecognizer
来呈现 UITableViewController
但你仍然需要再次创建一个自定义动画来 "fake" 你想要的效果。
编辑: 所以,一段时间过去了,现在有一个非常棒的库,叫做 Pulley。它完全符合我的要求,而且设置起来轻而易举!
原回答:
感谢 Rikh 和 Tj3n 给我的提示。我设法做了一些非常基本的事情,它没有像优步那样漂亮的动画,但它完成了工作。
使用下面的代码,你可以滑动任何UIViewController。我在图像上使用了 UIPanGestureRecognizer,它将始终位于拖动视图的顶部。基本上,您使用该图像,它会识别它被拖动的位置,并根据用户的输入设置视图的框架。
首先转到您的故事板并为将要拖动的 UIViewController 添加标识符。
然后在MainViewController中,使用如下代码:
class MainViewController: UIViewController {
// This image will be dragged up or down.
@IBOutlet var imageView: UIImageView!
// Gesture recognizer, will be added to image below.
var swipedOnImage = UIPanGestureRecognizer()
// This is the view controller that will be dragged with the image. In my case it's a UITableViewController.
var vc = UIViewController()
override func viewDidLoad() {
super.viewDidLoad()
// I'm using a storyboard.
let sb = UIStoryboard(name: "Main", bundle: nil)
// I have identified the view inside my storyboard.
vc = sb.instantiateViewController(withIdentifier: "TableVC")
// These values can be played around with, depending on how much you want the view to show up when it starts.
vc.view.frame = CGRect(x: 0, y: self.view.frame.height, width: self.view.frame.width, height: -300)
self.addChildViewController(vc)
self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
swipedOnImage = UIPanGestureRecognizer(target: self, action: #selector(self.swipedOnViewAction))
imageView.addGestureRecognizer(swipedOnImage)
imageView.isUserInteractionEnabled = true
}
// This function handles resizing of the tableview.
func swipedOnViewAction() {
let yLocationTouched = swipedOnImage.location(in: self.view).y
imageView.frame.origin.y = yLocationTouched
// These values can be played around with if required.
vc.view.frame = CGRect(x: 0, y: yLocationTouched, width: UIScreen.main.bounds.width, height: (UIScreen.main.bounds.height) - (yLocationTouched))
vc.view.frame.origin.y = yLocationTouched + 50
}
最终产品
现在,我的回答可能不是最有效的方法,但我是 iOS 的新手,所以这是我目前能想到的最好方法。
我知道这个问题已经有将近 2 年半的历史了,但以防万一有人通过搜索引擎找到这个问题:
我会说你最好的选择是使用 UIViewPropertyAnimator
。这里有一篇很棒的文章:http://www.swiftkickmobile.com/building-better-app-animations-swift-uiviewpropertyanimator/
编辑:
我设法得到一个使用 UIViewPropertyAnimator
的简单原型,这是它的 GIF:
这是 Github 上的项目:https://github.com/Luigi123/UIViewPropertyAnimatorExample
基本上我有两个UIViewController
,主要的ViewController
和次要的BottomSheetViewController
。辅助视图有一个 UIPanGestureRecognizer
使其可拖动,在识别器的回调函数中我做了 3 件事(在实际移动它之后):
①计算屏幕被拖动的百分比,
② 在辅助视图中触发动画,
③ 通知主视图有关拖动操作,以便它可以触发它的动画。在这种情况下,我使用 Notification
,将百分比传递给 notification.userInfo
.
我不确定如何表达①,举个例子,如果屏幕是 500 像素高,用户将次视图拖动到第 100 个像素,我计算出用户将它拖动了 20%一路攀升。这个百分比正是我需要传递到 UIViewPropertyAnimator
个实例中的 fractionComplete
属性 的百分比。
⚠️ 需要注意的一件事是,我无法让它与实际的导航栏一起工作,所以我使用了一个 "normal" 视图,并在其中放置了一个标签。
我尝试通过删除一些实用功能(例如检查用户交互是否完成)来使代码更小,但这意味着用户可以停止在屏幕中间拖动并且应用程序根本不会做出反应,所以我真的建议您在 github 存储库中查看完整代码。但好消息是,执行动画的整个代码大约有 100 行代码。
考虑到这一点,这是主屏幕的代码,ViewController
:
import UIKit
import MapKit
import NotificationCenter
class ViewController: UIViewController {
@IBOutlet weak var someView: UIView!
@IBOutlet weak var blackView: UIView!
var animator: UIViewPropertyAnimator?
func createBottomView() {
guard let sub = storyboard!.instantiateViewController(withIdentifier: "BottomSheetViewController") as? BottomSheetViewController else { return }
self.addChild(sub)
self.view.addSubview(sub.view)
sub.didMove(toParent: self)
sub.view.frame = CGRect(x: 0, y: view.frame.maxY - 100, width: view.frame.width, height: view.frame.height)
}
func subViewGotPanned(_ percentage: Int) {
guard let propAnimator = animator else {
animator = UIViewPropertyAnimator(duration: 3, curve: .linear, animations: {
self.blackView.alpha = 1
self.someView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8).concatenating(CGAffineTransform(translationX: 0, y: -20))
})
animator?.startAnimation()
animator?.pauseAnimation()
return
}
propAnimator.fractionComplete = CGFloat(percentage) / 100
}
func receiveNotification(_ notification: Notification) {
guard let percentage = notification.userInfo?["percentage"] as? Int else { return }
subViewGotPanned(percentage)
}
override func viewDidLoad() {
super.viewDidLoad()
createBottomView()
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil, using: receiveNotification(_:))
}
}
以及辅助视图的代码 (BottomSheetViewController
):
import UIKit
import NotificationCenter
class BottomSheetViewController: UIViewController, UIGestureRecognizerDelegate {
@IBOutlet weak var navBarView: UIView!
var panGestureRecognizer: UIPanGestureRecognizer?
var animator: UIViewPropertyAnimator?
override func viewDidLoad() {
gotPanned(0)
super.viewDidLoad()
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(respondToPanGesture))
view.addGestureRecognizer(gestureRecognizer)
gestureRecognizer.delegate = self
panGestureRecognizer = gestureRecognizer
}
func gotPanned(_ percentage: Int) {
if animator == nil {
animator = UIViewPropertyAnimator(duration: 1, curve: .linear, animations: {
let scaleTransform = CGAffineTransform(scaleX: 1, y: 5).concatenating(CGAffineTransform(translationX: 0, y: 240))
self.navBarView.transform = scaleTransform
self.navBarView.alpha = 0
})
animator?.isReversed = true
animator?.startAnimation()
animator?.pauseAnimation()
}
animator?.fractionComplete = CGFloat(percentage) / 100
}
// MARK: methods to make the view draggable
@objc func respondToPanGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
moveToY(self.view.frame.minY + translation.y)
recognizer.setTranslation(.zero, in: self.view)
}
private func moveToY(_ position: CGFloat) {
view.frame = CGRect(x: 0, y: position, width: view.frame.width, height: view.frame.height)
let maxHeight = view.frame.height - 100
let percentage = Int(100 - ((position * 100) / maxHeight))
gotPanned(percentage)
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.post(name: name, object: nil, userInfo: ["percentage": percentage])
}
}