web-dev-qa-db-ja.com

Mapsアプリの一番下のシートをどうやって模倣できますか。

IOS 10の新しいMapsアプリで一番下のシートを真似ることができる方法を教えてもらえますか。

Androidでは、この振る舞いを模倣するBottomSheetを使用できますが、iOSにはそのようなものは見つかりませんでした。

検索バーが一番下になるように、コンテンツがインセットされた単純なスクロールビューですか。

私はiOSプログラミングにかなり慣れていないので、誰かがこのレイアウトの作成を手伝ってくれるならば、それは大いに感謝されるでしょう。

これは私が "一番下のシート"と言う意味です。

screenshot of the collapsed bottom sheet in Maps

screenshot of the expanded bottom sheet in Maps

155
qwertz

新しいMapsアプリの一番下のシートが、ユーザーの操作にどのように反応するのか、正確にはわかりません。しかし、スクリーンショットのようなカスタムビューを作成してメインビューに追加することもできます。

私はあなたが方法を知っていると思います:

1 - ストーリーボードまたはxibファイルを使用してView Controllerを作成します。

2 - googleMapsまたはAppleのMapKitを使う。

1- MapViewController BottomSheetViewController のように、2つのビューコントローラを作成します。最初のコントローラーはマップをホストし、2番目は一番下のシート自体です。

MapViewControllerを設定する

下のシートビューを追加するためのメソッドを作成します。

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()
}

BottomSheetViewControllerを設定する

1)背景を準備する

ぼかし効果と鮮やかさ効果を追加するメソッドを作成する

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であることを確認してください。

2)ボトムシートの外観をアニメートする

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)
    }
}

3)必要に応じてxibを修正してください。

4)パンジェスチャー認識機能をビューに追加します。

あなたの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 プロパティを無効にします。

  • ビューは部分的に表示されています。
  • ビューは完全に表示され、scrollView contentOffset プロパティは0で、ユーザーはビューを下にドラッグしています。

それ以外の場合はスクロールを有効にします。

  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()関数はどのビューを一番下のシートとして使用するかを制御します。

サンプルプロジェクトのスクリーンショット

- 部分ビュー

enter image description here

- 全景

enter image description here

- スクロール表示

enter image description here

254
Ahmad Elassuty

2018年12月4日に更新

私はこのソリューションに基づいたライブラリをリリースしました 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 transition between the scroll and the translation

Mapsでは、お気づきかもしれませんが、tableViewがcontentOffset.y == 0に達すると、一番下のシートが上にスライドするか下に移動します。

私たちのパンジェスチャーが翻訳を開始するとき、私たちは単にスクロールを有効/無効にすることができないので、ポイントはトリッキーです。新しいタッチが始まるまでスクロールを止めます。これは、ここで提案されているほとんどの解決策の場合です。

これは私がこの運動を実行しようとしているところです。

出発点:地図アプリ

調査を開始するために、Mapsのビュー階層を視覚化しましょう(シミュレータでMapsを起動し、Xcode 9でDebug> Attach to process by PID or Name> Mapsを選択します)。

Maps debug view hierarchy

動きがどのように機能するかはわかりませんが、その論理を理解するのに役立ちました。 lldbとビュー階層デバッガで遊ぶことができます。

ViewControllerスタック

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()
    }
}

最後の問題:オーバーレイコンテナのタッチをディスパッチする

翻訳は今かなり効率的です。しかし、それでも最終的な問題があります。つまり、タッチが背景ビューに反映されていないということです。それらはすべて、オーバーレイコンテナのビューによって傍受されます。 isUserInteractionEnabledfalseに設定することはできません。テーブルビューでの対話も無効になるからです。解決策は、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()
}

結果

これが結果です。

Result

ここでコード を見つけることができます

あなたが何かバグを見たら、私に知らせてください!特にオーバーレイにヘッダを追加する場合は、実装ではもちろん2番目のパンジェスチャを使用できます。

更新日23/08/18

ユーザーがドラッグを終了したときに、スクロールのscrollViewDidEndDragging/willEndScrollingWithVelocityではなくenabledisableに置き換えることができます。

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)
}
41
GaétanZ

プーリーを試してください。

プーリーは、iOS 10のMapsアプリで引き出しを模倣するための使いやすい引き出しライブラリです。それはあなたが引き出しのコンテンツまたは主なコンテンツとして任意のUIViewControllerサブクラスを使用することを可能にする簡単なAPIを公開します。

Pulley Preview

https://github.com/52inc/Pulley

15
Rajee Jones

あなたは私の答えを試すことができます 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のような場所を検索するための一番下のシートを表示するための別のサンプルコードです。

Sample App like Apple Maps

8
SCENEE

テーブルビューと外側ビューの両方を同時にパンするのはスムーズではないので、上記のどれも私にとってはうまくいきません。だから私はios Mapsアプリで意図した動作を達成するために私自身のコードを書いた。

https://github.com/OfTheWolf/UBottomSheet

ubottom sheet

2
ugur

たぶん、あなたは私の答えを試すことができます https://github.com/AnYuan/AYPannel 、プーリーに触発された。引き出しの移動からリストのスクロールへのスムーズな移行。コンテナスクロールビューにパンジェスチャを追加し、YESを返すようにshouldRecognizeSimultaneouslyWithGestureRecognizerを設定しました。上記の私のgithubリンクでより詳細に。助けたい。

0
day