ฉันปล่อยห้องสมุดตามคำตอบด้านล่าง
มันเลียนแบบการซ้อนทับแอปพลิเคชันทางลัด ดูบทความนี้สำหรับรายละเอียด
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
แผ่นด้านล่างจะเลื่อนขึ้นหรือลง
จุดนี้เป็นเรื่องยุ่งยากเพราะเราไม่สามารถเปิด / ปิดการใช้งานการเลื่อนเมื่อท่าทางการแพนเริ่มการแปล มันจะหยุดการเลื่อนจนกว่าการสัมผัสใหม่จะเริ่มขึ้น นี่เป็นกรณีในโซลูชันส่วนใหญ่ที่นำเสนอที่นี่
นี่คือความพยายามของฉันที่จะใช้การเคลื่อนไหวนี้
จุดเริ่มต้น: แอป Maps
การเริ่มต้นการตรวจสอบของเราขอเห็นภาพลำดับชั้นมุมมองของแผนที่ (Maps บนเริ่มต้นจำลองและเลือก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
ได้
สำหรับตอนนี้มีเพียงวิธีการสาธารณะเพียงวิธีเดียวในการทำให้การเปลี่ยนแปลงตำแหน่งเปลี่ยนแปลงและมีมุมมองที่โปร่งใส:
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 callback ไม่มีโอกาสที่ตัวจดจำของเราจะเรียกใช้หลังจากที่ผู้แทนที่สำเร็จ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:
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
เพราะจะปิดใช้งานการโต้ตอบในมุมมองตารางของเรา การแก้ปัญหาเป็นวิธีที่ใช้กันอย่างหนาแน่นในแอป 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()
}
ผลลัพธ์
นี่คือผลลัพธ์:
คุณสามารถค้นหารหัสที่นี่
กรุณาถ้าคุณเห็นข้อบกพร่องใด ๆ แจ้งให้เราทราบ! โปรดทราบว่าการใช้งานของคุณสามารถใช้รูปแบบการเลื่อนที่สองโดยเฉพาะถ้าคุณเพิ่มส่วนหัวในการซ้อน
อัปเดต 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)
}