IOS 10の新しいMapsアプリで一番下のシートを真似ることができる方法を教えてもらえますか。
Androidでは、この振る舞いを模倣するBottomSheet
を使用できますが、iOSにはそのようなものは見つかりませんでした。
検索バーが一番下になるように、コンテンツがインセットされた単純なスクロールビューですか。
私はiOSプログラミングにかなり慣れていないので、誰かがこのレイアウトの作成を手伝ってくれるならば、それは大いに感謝されるでしょう。
これは私が "一番下のシート"と言う意味です。
新しいMapsアプリの一番下のシートが、ユーザーの操作にどのように反応するのか、正確にはわかりません。しかし、スクリーンショットのようなカスタムビューを作成してメインビューに追加することもできます。
私はあなたが方法を知っていると思います:
1 - ストーリーボードまたはxibファイルを使用してView Controllerを作成します。
2 - googleMapsまたはAppleのMapKitを使う。
1- MapViewController と BottomSheetViewController のように、2つのビューコントローラを作成します。最初のコントローラーはマップをホストし、2番目は一番下のシート自体です。
下のシートビューを追加するためのメソッドを作成します。
func addBottomSheetView() {
// 1- Init bottomSheetVC
let bottomSheetVC = BottomSheetViewController()
// 2- Add bottomSheetVC as a child view
self.addChildViewController(bottomSheetVC)
self.view.addSubview(bottomSheetVC.view)
bottomSheetVC.didMoveToParentViewController(self)
// 3- Adjust bottomSheet frame and initial position.
let height = view.frame.height
let width = view.frame.width
bottomSheetVC.view.frame = CGRectMake(0, self.view.frame.maxY, width, height)
}
そしてviewDidAppearメソッドでそれを呼び出します。
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
addBottomSheetView()
}
ぼかし効果と鮮やかさ効果を追加するメソッドを作成する
func prepareBackgroundView(){
let blurEffect = UIBlurEffect.init(style: .Dark)
let visualEffect = UIVisualEffectView.init(effect: blurEffect)
let bluredView = UIVisualEffectView.init(effect: blurEffect)
bluredView.contentView.addSubview(visualEffect)
visualEffect.frame = UIScreen.mainScreen().bounds
bluredView.frame = UIScreen.mainScreen().bounds
view.insertSubview(bluredView, atIndex: 0)
}
あなたのviewWillAppearでこのメソッドを呼び出します
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
コントローラのビューの背景色がclearColorであることを確認してください。
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animateWithDuration(0.3) { [weak self] in
let frame = self?.view.frame
let yComponent = UIScreen.mainScreen().bounds.height - 200
self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
}
}
あなたのviewDidLoadメソッドでUIPanGestureRecognizerを追加してください。
override func viewDidLoad() {
super.viewDidLoad()
let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
view.addGestureRecognizer(gesture)
}
そしてあなたのジェスチャー動作を実装します。
func panGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self.view)
let y = self.view.frame.minY
self.view.frame = CGRectMake(0, y + translation.y, view.frame.width, view.frame.height)
recognizer.setTranslation(CGPointZero, inView: self.view)
}
カスタムビューがスクロールビューまたはそれを継承する他のビューである場合は、2つの選択肢があります。
最初:
ヘッダービューを使用してビューをデザインし、ヘッダーにpanGestureを追加します。 (ユーザーエクスペリエンスの悪さ)。
秒:
1 - 下のシートビューにpanGestureを追加します。
2 - UIGestureRecognizerDelegate を実装し、panGestureデリゲートをコントローラーに設定します。
3- shouldRecognizeSimultaneouslyWith デリゲート関数を実装し、 disable scrollView isScrollEnabled プロパティを無効にします。
それ以外の場合はスクロールを有効にします。
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
let gesture = (gestureRecognizer as! UIPanGestureRecognizer)
let direction = gesture.velocity(in: view).y
let y = view.frame.minY
if (y == fullView && tableView.contentOffset.y == 0 && direction > 0) || (y == partialView) {
tableView.isScrollEnabled = false
} else {
tableView.isScrollEnabled = true
}
return false
}
NOTE
サンプルプロジェクトのように、アニメーションオプションとして.allowUserInteractionを設定した場合 、アニメーション完了クロージャでスクロールを有効にする必要があります。ユーザーが上にスクロールしています
私は this レポに関するオプションを追加してサンプルプロジェクトを作成しました。これはフローをカスタマイズする方法についてより良い洞察を与えるかもしれません。
デモでは、addBottomSheetView()関数はどのビューを一番下のシートとして使用するかを制御します。
私はこのソリューションに基づいたライブラリをリリースしました https://github.com/applidium/ADOverlayContainer 。
ショートカットアプリのオーバーレイを模倣しています。詳しくは この記事 をご覧ください。
ライブラリの主要コンポーネントはOverlayContainerViewController
です。 View Controllerを上下にドラッグして、その下にあるコンテンツを隠したり表示したりできる領域を定義します。
let contentController = MapsViewController()
let overlayController = SearchViewController()
let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
contentController,
overlayController
]
window?.rootViewController = containerController
希望するノッチ数を指定するためにOverlayContainerViewControllerDelegate
を実装します。
enum OverlayNotch: Int, CaseIterable {
case minimum, medium, maximum
}
func numberOfNotches(in containerViewController: OverlayContainerViewController) -> Int {
return OverlayNotch.allCases.count
}
func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
heightForNotchAt index: Int,
availableSpace: CGFloat) -> CGFloat {
switch OverlayNotch.allCases[index] {
case .maximum:
return availableSpace * 3 / 4
case .medium:
return availableSpace / 2
case .minimum:
return availableSpace * 1 / 4
}
}
私は提案された解決策で扱われていない重要な点があると思います:スクロールと翻訳の間の移行。
Mapsでは、お気づきかもしれませんが、tableViewがcontentOffset.y == 0
に達すると、一番下のシートが上にスライドするか下に移動します。
私たちのパンジェスチャーが翻訳を開始するとき、私たちは単にスクロールを有効/無効にすることができないので、ポイントはトリッキーです。新しいタッチが始まるまでスクロールを止めます。これは、ここで提案されているほとんどの解決策の場合です。
これは私がこの運動を実行しようとしているところです。
調査を開始するために、Mapsのビュー階層を視覚化しましょう(シミュレータでMapsを起動し、Xcode 9でDebug
> Attach to process by PID or Name
> Maps
を選択します)。
動きがどのように機能するかはわかりませんが、その論理を理解するのに役立ちました。 lldbとビュー階層デバッガで遊ぶことができます。
Maps ViewControllerアーキテクチャの基本バージョンを作成しましょう。
BackgroundViewController
(マップビュー)から始めます。
class BackgroundViewController: UIViewController {
override func loadView() {
view = MKMapView()
}
}
TableViewを専用のUIViewController
に入れます。
class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
lazy var tableView = UITableView()
override func loadView() {
view = tableView
tableView.dataSource = self
tableView.delegate = self
}
[...]
}
それでは、オーバーレイを埋め込んでその翻訳を管理するためのVCが必要です。問題を単純化するために、オーバーレイをある静的なポイントOverlayPosition.maximum
から別の静的なOverlayPosition.minimum
に変換できると考えます。
今のところ、位置の変化をアニメートするためのパブリックメソッドは1つだけで、透明なビューがあります。
enum OverlayPosition {
case maximum, minimum
}
class OverlayContainerViewController: UIViewController {
let overlayViewController: OverlayViewController
var translatedViewHeightContraint = ...
override func loadView() {
view = UIView()
}
func moveOverlay(to position: OverlayPosition) {
[...]
}
}
最後に、全部を埋め込むにはViewControllerが必要です。
class StackViewController: UIViewController {
private var viewControllers: [UIViewController]
override func viewDidLoad() {
super.viewDidLoad()
viewControllers.forEach { gz_addChild($0, in: view) }
}
}
AppDelegateでは、起動シーケンスは次のようになります。
let overlay = OverlayViewController()
let containerViewController = OverlayContainerViewController(overlayViewController: overlay)
let backgroundViewController = BackgroundViewController()
window?.rootViewController = StackViewController(viewControllers: [backgroundViewController, containerViewController])
それでは、オーバーレイをどのように翻訳しますか。
提案された解決策の大部分は専用のパンジェスチャー認識装置を使用します、しかし我々は実際にすでにそれを持っています:テーブルビューのパンジェスチャー。さらに、スクロールと翻訳を同期させる必要があり、UIScrollViewDelegate
には必要なすべてのイベントがあります。
素朴な実装では、2回目のジェスチャを使用して、変換が発生したときにテーブルビューのcontentOffset
をリセットしようとします。
func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
if isTranslating {
tableView.contentOffset = .zero
}
}
しかし、うまくいきません。 tableViewは、自身のパンジェスチャ認識機能のアクションがトリガされたとき、またはdisplayLinkコールバックが呼び出されたときに、contentOffset
を更新します。これらのレコグナイザーがcontentOffset
を正常にオーバーライドするためにそれらの直後にトリガーする可能性はありません。私たちの唯一のチャンスは、(スクロールビューの各フレームでスクロールビュー呼び出しのlayoutSubviews
をオーバーライドすることによって)レイアウトフェーズに参加するか、didScroll
が変更されるたびに呼び出されるデリゲートのcontentOffset
メソッドに応答することです。これを試してみましょう。
Scrollviewのイベントを翻訳ハンドラであるOverlayVC
にディスパッチするために、OverlayContainerViewController
にデリゲートを追加します。
protocol OverlayViewControllerDelegate: class {
func scrollViewDidScroll(_ scrollView: UIScrollView)
func scrollViewDidStopScrolling(_ scrollView: UIScrollView)
}
class OverlayViewController: UIViewController {
[...]
func scrollViewDidScroll(_ scrollView: UIScrollView) {
delegate?.scrollViewDidScroll(scrollView)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
delegate?.scrollViewDidStopScrolling(scrollView)
}
}
私たちのコンテナの中で、私たちはenumを使って翻訳を追跡します:
enum OverlayInFlightPosition {
case minimum
case maximum
case progressing
}
現在位置の計算は次のようになります。
private var overlayInFlightPosition: OverlayInFlightPosition {
let height = translatedViewHeightContraint.constant
if height == maximumHeight {
return .maximum
} else if height == minimumHeight {
return .minimum
} else {
return .progressing
}
}
翻訳を処理するには3つの方法が必要です。
最初のものは翻訳を始める必要があるかどうかを教えてくれます。
private func shouldTranslateView(following scrollView: UIScrollView) -> Bool {
guard scrollView.isTracking else { return false }
let offset = scrollView.contentOffset.y
switch overlayInFlightPosition {
case .maximum:
return offset < 0
case .minimum:
return offset > 0
case .progressing:
return true
}
}
2番目のものは翻訳を実行します。これはscrollViewのパンジェスチャのtranslation(in:)
メソッドを使用します。
private func translateView(following scrollView: UIScrollView) {
scrollView.contentOffset = .zero
let translation = translatedViewTargetHeight - scrollView.panGestureRecognizer.translation(in: view).y
translatedViewHeightContraint.constant = max(
Constant.minimumHeight,
min(translation, Constant.maximumHeight)
)
}
3つめは、ユーザーが指を離したときに翻訳の終わりをアニメートします。ビューの速度と現在位置を使用して位置を計算します。
private func animateTranslationEnd() {
let position: OverlayPosition = // ... calculation based on the current overlay position & velocity
moveOverlay(to: position)
}
オーバーレイのデリゲート実装は、単純に次のようになります。
class OverlayContainerViewController: UIViewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard shouldTranslateView(following: scrollView) else { return }
translateView(following: scrollView)
}
func scrollViewDidStopScrolling(_ scrollView: UIScrollView) {
// prevent scroll animation when the translation animation ends
scrollView.isEnabled = false
scrollView.isEnabled = true
animateTranslationEnd()
}
}
翻訳は今かなり効率的です。しかし、それでも最終的な問題があります。つまり、タッチが背景ビューに反映されていないということです。それらはすべて、オーバーレイコンテナのビューによって傍受されます。 isUserInteractionEnabled
をfalse
に設定することはできません。テーブルビューでの対話も無効になるからです。解決策は、MapsアプリでPassThroughView
で大量に使用されているものです。
class PassThroughView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self {
return nil
}
return view
}
}
レスポンダチェーンから自分自身を削除します。
OverlayContainerViewController
内:
override func loadView() {
view = PassThroughView()
}
これが結果です。
ここでコード を見つけることができます 。
あなたが何かバグを見たら、私に知らせてください!特にオーバーレイにヘッダを追加する場合は、実装ではもちろん2番目のパンジェスチャを使用できます。
ユーザーがドラッグを終了したときに、スクロールのscrollViewDidEndDragging
/willEndScrollingWithVelocity
ではなくenable
をdisable
に置き換えることができます。
func scrollView(_ scrollView: UIScrollView,
willEndScrollingWithVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
switch overlayInFlightPosition {
case .maximum:
break
case .minimum, .progressing:
targetContentOffset.pointee = .zero
}
animateTranslationEnd(following: scrollView)
}
春のアニメーションを使用して、モーションフローをより良くするためにアニメーション化しながらユーザーの操作を可能にすることができます。
func moveOverlay(to position: OverlayPosition,
duration: TimeInterval,
velocity: CGPoint) {
overlayPosition = position
translatedViewHeightContraint.constant = translatedViewTargetHeight
UIView.animate(
withDuration: duration,
delay: 0,
usingSpringWithDamping: velocity.y == 0 ? 1 : 0.6,
initialSpringVelocity: abs(velocity.y),
options: [.allowUserInteraction],
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
プーリーを試してください。
プーリーは、iOS 10のMapsアプリで引き出しを模倣するための使いやすい引き出しライブラリです。それはあなたが引き出しのコンテンツまたは主なコンテンツとして任意のUIViewControllerサブクラスを使用することを可能にする簡単なAPIを公開します。
あなたは私の答えを試すことができます https://github.com/SCENEE/FloatingPanel 。これは、「ボトムシート」インターフェースを表示するためのコンテナビューコントローラを提供します。
使い方は簡単で、ジェスチャ認識機能を使用する必要はありません。必要に応じて、下のシートでスクロールビュー(または兄弟ビュー)を追跡することもできます。
これは簡単な例です。ボトムシートにコンテンツを表示するには、View Controllerを準備する必要があります。
import UIKit
import FloatingPanel
class ViewController: UIViewController {
var fpc: FloatingPanelController!
override func viewDidLoad() {
super.viewDidLoad()
fpc = FloatingPanelController()
// Add "bottom sheet" in self.view.
fpc.add(toParent: self)
// Add a view controller to display your contents in "bottom sheet".
let contentVC = ContentViewController()
fpc.set(contentViewController: contentVC)
// Track a scroll view in "bottom sheet" content if needed.
fpc.track(scrollView: contentVC.tableView)
}
...
}
ここで は、Apple Mapsのような場所を検索するための一番下のシートを表示するための別のサンプルコードです。
テーブルビューと外側ビューの両方を同時にパンするのはスムーズではないので、上記のどれも私にとってはうまくいきません。だから私はios Mapsアプリで意図した動作を達成するために私自身のコードを書いた。
たぶん、あなたは私の答えを試すことができます https://github.com/AnYuan/AYPannel 、プーリーに触発された。引き出しの移動からリストのスクロールへのスムーズな移行。コンテナスクロールビューにパンジェスチャを追加し、YESを返すようにshouldRecognizeSimultaneouslyWithGestureRecognizerを設定しました。上記の私のgithubリンクでより詳細に。助けたい。