누구나 iOS 10의 새로운지도 앱에서 하단 시트를 모방하는 방법을 말해 줄 수 있습니까?
안드로이드에서는 BottomSheet이 동작을 모방 한 것을 사용할 수 있지만 iOS에서는 이와 같은 것을 찾을 수 없습니다.
검색 막대가 맨 아래에 있도록 내용이 삽입 된 간단한 스크롤보기입니까?
나는 iOS 프로그래밍에 익숙하지 않기 때문에 누군가이 레이아웃을 만들 수 있다면 매우 높이 평가 될 것입니다.
이것이 "바닥 시트"의 의미입니다.
누구나 iOS 10의 새로운지도 앱에서 하단 시트를 모방하는 방법을 말해 줄 수 있습니까?
안드로이드에서는 BottomSheet이 동작을 모방 한 것을 사용할 수 있지만 iOS에서는 이와 같은 것을 찾을 수 없습니다.
검색 막대가 맨 아래에 있도록 내용이 삽입 된 간단한 스크롤보기입니까?
나는 iOS 프로그래밍에 익숙하지 않기 때문에 누군가이 레이아웃을 만들 수 있다면 매우 높이 평가 될 것입니다.
이것이 "바닥 시트"의 의미입니다.
답변:
새지도 앱의 맨 아래 시트가 사용자 상호 작용에 정확히 어떻게 반응하는지 모르겠습니다. 그러나 스크린 샷에서 보이는 것과 같은 사용자 정의보기를 만들어 기본보기에 추가 할 수 있습니다.
나는 당신이 방법을 알고 있다고 가정합니다 :
1- 스토리 보드 또는 xib 파일을 사용하여 뷰 컨트롤러를 만듭니다.
2- googleMaps 또는 Apple의 MapKit을 사용하십시오.
1- ViewView 컨트롤러 2 개 (예 : MapViewController 및 BottomSheetViewController)를 만듭니다 . 첫 번째 컨트롤러는 맵을 호스팅하고 두 번째 컨트롤러는 바텀 시트 자체입니다.
바텀 시트보기를 추가하는 방법을 작성하십시오.
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)
}
뷰에서이 메소드를 호출하십시오.
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)
}
사용자 정의보기가 스크롤보기 또는 상속 된 다른보기 인 경우 두 가지 옵션이 있습니다.
먼저:
헤더 뷰를 사용하여 뷰를 디자인하고 panGesture를 헤더에 추가하십시오. (나쁜 사용자 경험) .
둘째:
1-팬 시트를 하단 시트보기에 추가합니다.
2- UIGestureRecognizerDelegate를 구현하고 panGesture 델리게이트를 컨트롤러로 설정하십시오.
3- delegateRecognizeSimultaneously with delegate 기능을 구현 하고 두 가지 경우에 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엑스 코드 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()
}
결과는 다음과 같습니다.
코드는 여기에서 찾을 수 있습니다 .
버그가 있으면 알려주십시오! 물론 오버레이에 헤더를 추가하는 경우 구현시 두 번째 팬 제스처를 사용할 수 있습니다.
우리는 대체 할 수 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 프로세스에 연결할 수있는 것을 포함하여 루트가 일반적으로 제공하는 모든 기능을 사용할 수 있습니다.
도르래를 보십시오 :
풀리는 iOS 10의지도 앱에서 서랍을 모방하기위한 사용하기 쉬운 서랍 라이브러리입니다. UIViewController 서브 클래스를 드로어 컨텐츠 또는 기본 컨텐츠로 사용할 수있는 간단한 API를 제공합니다.

당신은 내 대답 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와 같은 위치를 검색하기 위해 바텀 시트를 표시하는 또 다른 예제 코드입니다.
ios Maps 앱에서 의도 한 동작을 달성하기 위해 내 라이브러리를 작성했습니다. 프로토콜 지향 솔루션입니다. 따라서 기본 클래스를 상속하지 않아도 시트 컨트롤러를 만들고 원하는대로 구성 할 수 있습니다. 또한 UINavigationController가 있거나없는 내부 탐색 / 프레젠테이션을 지원합니다.
자세한 내용은 아래 링크를 참조하십시오.
최근 에 Swift 5.1로 작성된 SwipeableView하위 클래스 라는 구성 요소를 만들었습니다 UIView. 4 가지 방향을 모두 지원하고 여러 가지 사용자 정의 옵션이 있으며 다양한 속성 및 항목 (예 : 레이아웃 제약 조건, 배경 / 색조 색상, 아핀 변환, 알파 채널 및 뷰 센터와 같은 각 쇼케이스로 데모)을 애니메이션하고 보간 할 수 있습니다. 또한 설정 또는 자동 감지 된 경우 내부 스크롤보기와 스 와이프 조정을 지원합니다. 매우 쉽고 간단하게 사용해야합니다 (희망 🙂)
링크 https://github.com/LucaIaco/SwipeableView
개념의 증거:
그것이 도움이되기를 바랍니다.
어쩌면 Pulley에서 영감을 얻은 내 대답 https://github.com/AnYuan/AYPannel을 시도해 볼 수 있습니다 . 서랍 이동에서 목록 스크롤로 부드럽게 전환합니다. 컨테이너 스크롤보기에 팬 제스처를 추가하고 shouldRecognizeSimultaneouslyWithGestureRecognizer를 YES로 반환하도록 설정했습니다. 위의 github 링크에서 자세한 내용을 확인하십시오. 도와주세요.