[ios] 지도 앱에서 바텀 시트를 어떻게 모방 할 수 있습니까?

누구나 iOS 10의 새로운지도 앱에서 하단 시트를 모방하는 방법을 말해 줄 수 있습니까?

안드로이드에서는 BottomSheet이 동작을 모방 한 것을 사용할 수 있지만 iOS에서는 이와 같은 것을 찾을 수 없습니다.

검색 막대가 맨 아래에 있도록 내용이 삽입 된 간단한 스크롤보기입니까?

나는 iOS 프로그래밍에 익숙하지 않기 때문에 누군가이 레이아웃을 만들 수 있다면 매우 높이 평가 될 것입니다.

이것이 “바닥 시트”의 의미입니다.

지도에서 접힌 바텀 시트의 스크린 샷

지도에서 확장 된 하단 시트의 스크린 샷



답변

새지도 앱의 맨 아래 시트가 사용자 상호 작용에 정확히 어떻게 반응하는지 모르겠습니다. 그러나 스크린 샷에서 보이는 것과 같은 사용자 정의보기를 만들어 기본보기에 추가 할 수 있습니다.

나는 당신이 방법을 알고 있다고 가정합니다 :

1- 스토리 보드 또는 xib 파일을 사용하여 뷰 컨트롤러를 만듭니다.

2- googleMaps 또는 Apple의 MapKit을 사용하십시오.

1- ViewView 컨트롤러 2 개 (예 : MapViewControllerBottomSheetViewController)를 만듭니다 . 첫 번째 컨트롤러는 맵을 호스팅하고 두 번째 컨트롤러는 바텀 시트 자체입니다.

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

뷰에서이 메소드를 호출하십시오.

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

스크롤 가능한 바텀 시트 :

사용자 정의보기가 스크롤보기 또는 상속 된 다른보기 인 경우 두 가지 옵션이 있습니다.

먼저:

헤더 뷰를 사용하여 뷰를 디자인하고 panGesture를 헤더에 추가하십시오. (나쁜 사용자 경험) .

둘째:

1-팬 시트를 하단 시트보기에 추가합니다.

2- UIGestureRecognizerDelegate를 구현하고 panGesture 델리게이트를 컨트롤러로 설정하십시오.

3- delegateRecognizeSimultaneously with delegate 기능을 구현 하고 두 가지 경우에 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
  }

노트

혹시 설정 .allowUserInteraction를 , 샘플 프로젝트처럼, 애니메이션 옵션으로 사용하면 사용자가 스크롤되는 경우 애니메이션 완료 폐쇄에 스크롤을 활성화해야하므로.

샘플 프로젝트

리포지토리 에 더 많은 옵션 이 포함 된 샘플 프로젝트를 만들었습니다 . 그러면 흐름을 사용자 지정하는 방법에 대한 더 나은 통찰력을 얻을 수 있습니다.

데모에서 addBottomSheetView () 함수는 바텀 시트로 사용할 뷰를 제어합니다.

샘플 프로젝트 스크린 샷

-부분보기

여기에 이미지 설명을 입력하십시오

-FullView

여기에 이미지 설명을 입력하십시오

-스크롤 가능보기

여기에 이미지 설명을 입력하십시오


답변

아래 답변에 따라 도서관을 출시했습니다 .

바로 가기 응용 프로그램 오버레이를 모방합니다. 자세한 내용은 이 기사 를 참조하십시오.

라이브러리의 주요 구성 요소는 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엑스 코드 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 .

현재 위치 변경에 애니메이션을 적용하는 공개 방법은 하나 뿐이며 투명하게 볼 수 있습니다.

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 필요한 모든 이벤트를 유지해야합니다!

순진한 구현은 두 번째 팬 제스처를 사용 contentOffset하고 변환이 발생할 때 테이블 뷰의 재설정을 시도 합니다.

func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
    if isTranslating {
        tableView.contentOffset = .zero
    }
}

그러나 작동하지 않습니다. tableView contentOffset는 자체 팬 제스처 인식기 동작이 트리거되거나 displayLink 콜백이 호출 될 때 해당 테이블을 업데이트합니다 . 바로 그 후 우리의 인식 트리거가 성공적를 오버라이드 (override) 할 수 있다는 기회가 없습니다 contentOffset. 우리의 유일한 기회는 레이아웃 단계에 참여하거나 ( layoutSubviews스크롤보기의 각 프레임에서 스크롤보기 호출을 재정 의 함) 또는 수정 didScroll될 때마다 호출되는 대리자 의 메서드에 응답하는 것입니다 contentOffset. 이것을 시도해 봅시다.

번역 구현

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

두 번째는 번역을 수행합니다. 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)
    )
}

세 번째는 사용자가 손가락을 떼면 번역의 끝을 애니메이션으로 만듭니다. 뷰의 속도와 현재 위치를 사용하여 위치를 계산합니다.

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

결과

결과는 다음과 같습니다.

결과

코드는 여기에서 찾을 수 있습니다 .

버그가 있으면 알려주십시오! 물론 오버레이에 헤더를 추가하는 경우 구현시 두 번째 팬 제스처를 사용할 수 있습니다.

23/08/18 업데이트

우리는 대체 할 수 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)
}


답변

도르래를 보십시오 :

풀리는 iOS 10의지도 앱에서 서랍을 모방하기위한 사용하기 쉬운 서랍 라이브러리입니다. UIViewController 서브 클래스를 드로어 컨텐츠 또는 기본 컨텐츠로 사용할 수있는 간단한 API를 제공합니다.

풀리 미리보기

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


답변

당신은 내 대답 https://github.com/SCENEE/FloatingPanel을 시도 할 수 있습니다 . “하단 시트”인터페이스를 표시하는 컨테이너 뷰 컨트롤러를 제공합니다.

사용하기 쉽고 제스처 인식기 처리에 신경 쓰지 않습니다! 또한 필요한 경우 맨 아래 시트에서 스크롤보기 (또는 형제보기)를 추적 할 수 있습니다.

이것은 간단한 예입니다. 하단 시트에 콘텐츠를 표시하려면 뷰 컨트롤러를 준비해야합니다.

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와 같은 위치를 검색하기 위해 바텀 시트를 표시하는 또 다른 예제 코드입니다.

Apple Maps와 같은 샘플 앱


답변

ios Maps 앱에서 의도 한 동작을 달성하기 위해 내 라이브러리를 작성했습니다. 프로토콜 지향 솔루션입니다. 따라서 기본 클래스를 상속하지 않아도 시트 컨트롤러를 만들고 원하는대로 구성 할 수 있습니다. 또한 UINavigationController가 있거나없는 내부 탐색 / 프레젠테이션을 지원합니다.

자세한 내용은 아래 링크를 참조하십시오.

https://github.com/OfTheWolf/UBottomSheet

우 보텀 시트


답변

최근 에 Swift 5.1로 작성된 SwipeableView하위 클래스 라는 구성 요소를 만들었습니다 UIView. 4 가지 방향을 모두 지원하고 여러 가지 사용자 정의 옵션이 있으며 다양한 속성 및 항목 (예 : 레이아웃 제약 조건, 배경 / 색조 색상, 아핀 변환, 알파 채널 및 뷰 센터와 같은 각 쇼케이스로 데모)을 애니메이션하고 보간 할 수 있습니다. 또한 설정 또는 자동 감지 된 경우 내부 스크롤보기와 스 와이프 조정을 지원합니다. 매우 쉽고 간단하게 사용해야합니다 (희망 ?)

링크 https://github.com/LucaIaco/SwipeableView

개념의 증거:

여기에 이미지 설명을 입력하십시오

그것이 도움이되기를 바랍니다.


답변

어쩌면 Pulley에서 영감을 얻은 내 대답 https://github.com/AnYuan/AYPannel을 시도해 볼 수 있습니다 . 서랍 이동에서 목록 스크롤로 부드럽게 전환합니다. 컨테이너 스크롤보기에 팬 제스처를 추가하고 shouldRecognizeSimultaneouslyWithGestureRecognizer를 YES로 반환하도록 설정했습니다. 위의 github 링크에서 자세한 내용을 확인하십시오. 도와주세요.