iOS 10の新しいマップアプリのボトムシートを模倣する方法を誰かに教えてもらえますか?
Androidでは、BottomSheet
この動作を模倣するを使用できますが、iOSの場合、そのようなものは見つかりませんでした。
検索バーが下部にあるように、コンテンツがはめ込まれた単純なスクロールビューですか?
私はiOSプログラミングにかなり慣れていないので、誰かがこのレイアウトの作成を手伝ってくれるとしたら、それは高く評価されます。
これが「ボトムシート」の意味です。
iOS 10の新しいマップアプリのボトムシートを模倣する方法を誰かに教えてもらえますか?
Androidでは、BottomSheet
この動作を模倣するを使用できますが、iOSの場合、そのようなものは見つかりませんでした。
検索バーが下部にあるように、コンテンツがはめ込まれた単純なスクロールビューですか?
私はiOSプログラミングにかなり慣れていないので、誰かがこのレイアウトの作成を手伝ってくれるとしたら、それは高く評価されます。
これが「ボトムシート」の意味です。
回答:
新しいマップアプリの一番下のシートがユーザーの操作にどのように反応するのか正確にはわかりません。しかし、スクリーンショットのようなカスタムビューを作成して、メインビューに追加することができます。
私はあなたが次の方法を知っていると思います:
1-ストーリーボードまたはxibファイルを使用して、ビューコントローラーを作成します。
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デリゲート関数を実装し、2つのケースで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
}
注意
サンプルプロジェクトのように、.allowUserInteractionをアニメーションオプションとして設定した場合、ユーザーが上にスクロールしている場合は、アニメーション完了クロージャーでスクロールを有効にする必要があります。
このリポジトリでより多くのオプションを使用してサンプルプロジェクトを作成しました。フローをカスタマイズする方法についてより良い洞察を得ることができます。
デモでは、addBottomSheetView()関数が、ボトムシートとして使用するビューを制御します。
以下の回答に基づいてライブラリをリリースしました。
これは、ショートカットアプリケーションオーバーレイを模倣しています。詳細については、この記事を参照してください。
ライブラリの主なコンポーネントはOverlayContainerViewController
です。これは、ビューコントローラーを上下にドラッグして、その下のコンテンツを非表示または表示できる領域を定義します。
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
}
}
提案された解決策では扱われない重要な点があると思います。スクロールと翻訳の間の移行です。
マップでは、お気づきかもしれませんが、tableViewがに達するcontentOffset.y == 0
と、下のシートが上または下にスライドします。
パンジェスチャが翻訳を開始するときにスクロールを有効または無効にすることはできないため、要点は注意が必要です。新しいタッチが始まるまでスクロールを停止します。これは、ここで提案されているソリューションのほとんどに当てはまります。
これが、この動きを実装するための私の試みです。
私たちの調査を開始するには、聞かせてのは、地図のビュー階層を可視化する(シミュレータ上で地図を起動し、選択Debug
> Attach to process by PID or Name
> Maps
Xcodeの9)。
モーションの仕組みはわかりませんが、そのロジックを理解するのに役立ちました。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はcontentOffset
、独自のパンジェスチャーレコグナイザーアクションがトリガーされたとき、またはそのdisplayLinkコールバックが呼び出されたときに更新します。それらの直後に認識エンジンがトリガーして、をオーバーライドする可能性はありませんcontentOffset
。唯一のチャンスは、レイアウト段階(layoutSubviews
スクロールビューの各フレームでスクロールビューの呼び出しをオーバーライドする)に参加するかdidScroll
、contentOffset
が変更されるたびに呼び出されるデリゲートのメソッドに応答することです。これを試してみましょう。
にデリゲートを追加してOverlayVC
、scrollviewのイベントを翻訳ハンドラである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 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つ目は変換を実行します。translation(in:)
scrollViewのパンジェスチャのメソッドを使用します。
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
ことはできません。テーブルビューでの相互作用も無効になるからです。解決策は、マップアプリで大量に使用されるものです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
はなくenabling
/で置き換えることができdisabling
ます。
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)
}
UIViewPropertyAnimator
(一時停止する機能がある)を使用して解決でき、fractionComplete
プロパティを使用してスクラブを実行できると思います。
translateView(following:)
方法では、平行移動に基づいて高さを計算しますが、0.0以外の値から開始する可能性があります(たとえば、ドラッグすると、テーブルビューが少しスクロールされ、オーバーレイが最大化されます)。それは最初のジャンプになります。これを解決するには、テーブルビューのscrollViewWillBeginDragging
初期値contentOffset
を覚えておき、それをtranslateView(following:)
の計算で使用します。
csrutil disable && reboot
。これで、Appleプロセスにアタッチできることを含め、rootが通常提供するすべての権限を取得できます。
プーリーを試してください:
Pulleyは、iOS 10のマップアプリで引き出しを模倣することを目的とした使いやすい引き出しライブラリです。これは、任意の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マップアプリで意図した動作を実現するために、独自のライブラリを作成しました。プロトコル指向のソリューションです。したがって、基本クラスを継承する必要はなく、代わりにシートコントローラを作成して、必要に応じて構成します。また、UINavigationControllerの有無にかかわらず、内部ナビゲーション/プレゼンテーションをサポートします。
詳細については、以下のリンクを参照してください。
私は最近、Swift 5.1で書かSwipeableView
れたのサブクラスと呼ばれるコンポーネントを作成しましたUIView
。4つの方向すべてをサポートし、いくつかのカスタマイズオプションがあり、さまざまな属性とアイテム(レイアウト制約、背景/ティントカラー、アフィン変換、アルファチャネル、ビューセンターなど)をアニメーション化および補間できます。これらはすべて、それぞれのショーケースでデモされます。また、設定または自動検出された場合は、内側のスクロールビューとのスワイプ調整をサポートします。使用するのは非常に簡単で簡単でなければなりません(私はhopeを願っています)
https://github.com/LucaIaco/SwipeableViewのリンク
コンセプトの証明:
それが役に立てば幸い
たぶん、あなたは私の答えを試してみることができますhttps://github.com/AnYuan/AYPannel、プーリーに触発されました。引き出しの移動からリストのスクロールへのスムーズな移行。コンテナースクロールビューにパンジェスチャーを追加し、shouldRecognizeSimultaneouslyWithGestureRecognizerをYESに設定しました。上記の私のgithubリンクの詳細。助けて欲しい。