누구나 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 링크에서 자세한 내용을 확인하십시오. 도와주세요.