मैंने नीचे अपने उत्तर के आधार पर एक पुस्तकालय जारी किया ।
यह शॉर्टकट एप्लिकेशन ओवरले की नकल करता है। देखें इस लेख जानकारी के लिए।
पुस्तकालय का मुख्य घटक पुस्तकालय है 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
}
}
पिछला उत्तर
मुझे लगता है कि एक महत्वपूर्ण बिंदु है जो सुझाए गए समाधानों में व्यवहार नहीं किया जाता है: स्क्रॉल और अनुवाद के बीच संक्रमण।
मैप्स में, जैसा कि आपने देखा होगा, जब टेबल व्यू पहुंचता है contentOffset.y == 0
, तो नीचे की शीट या तो स्लाइड करती है या नीचे जाती है।
जब हम हमारे पैन जेस्चर का अनुवाद शुरू करते हैं तो हम इस बिंदु पर मुश्किल हो जाते हैं क्योंकि हम बस स्क्रॉल को सक्षम / अक्षम नहीं कर सकते हैं। यह एक नया स्पर्श शुरू होने तक स्क्रॉल को रोक देगा। यहाँ अधिकांश प्रस्तावित समाधानों में यही स्थिति है।
यहाँ इस प्रस्ताव को लागू करने का मेरा प्रयास है।
प्रारंभिक बिंदु: मैप्स ऐप
हमारी जांच शुरू करने के लिए, चलो नक्शे को देखते पदानुक्रम कल्पना (एक सिम्युलेटर पर मानचित्र प्रारंभ करें और चयन Debug
> Attach to process by PID or Name
> Maps
Xcode 9 में)।
यह नहीं बताता कि गति कैसे काम करती है, लेकिन इसने मुझे इसके तर्क को समझने में मदद की। आप एलडीबी और व्यू पदानुक्रम डिबगर के साथ खेल सकते हैं।
हमारे दृश्य नियंत्रक ढेर
चलो मैप्स व्यू कंट्रोलर आर्किटेक्चर का एक मूल संस्करण बनाते हैं।
हम एक 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
}
}
लेकिन यह काम नहीं करता है। contentOffset
जब अपने स्वयं के पैन जेस्चर पहचानकर्ता कार्रवाई ट्रिगर होती है या जब इसका डिस्प्लेलिंक कॉलबैक कहा जाता है, तो टेबल व्यू इसे अपडेट करता है। ऐसा कोई मौका नहीं है कि हमारे पहचानकर्ता सफलतापूर्वक ओवरराइड करने के लिए सही होने के बाद ट्रिगर होते हैं 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:)
स्क्रॉलव्यू के पैन जेस्चर की विधि का उपयोग करता है ।
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)
}