如何从嵌入在 UIScrollView 中的 UiView 中的 CollectionView 接收触摸事件

How to receive touch events from a CollectionView embedded inside a UiView embedded inside a UIScrollView

我有一个很长的表单并创建了一个包含自定义日历和几个文本字段的容器视图 (UIView)。 UIView 嵌入在 scrollView 中,这样我就可以滚动长表单。

ScrollView
   -ContainerView // UIView
       -CustomCalendar // contains a UICollectionView
       -TextField
       -TextField
        //etc etc..

苹果说:

You should not embed UIWebView or UITableView objects in UIScrollView objects. If you do so, unexpected behavior can result because touch events for the two objects can be mixed up and wrongly handled.

为了解决这个问题,我尝试禁用 CalendarView 上的滚动,但没有任何区别:

// this is inside the CalenderView file
myCollectionView.isScrollEnabled = false

我从 here and here 获得了日历,它使用 collectionView 来显示日期和选择日期时触发 didSelectItem。

问题是因为自定义日历包含一个 collectionView 并且当我触摸一个日期时它在 scrollView 中 didSelecetItem 不会 运行。

如何让日历的collectionView接收触摸事件?

日历不在 scrollView 内时工作正常

let scrollView: UIScrollView = {
    let sv = UIScrollView()
    sv.translatesAutoresizingMaskIntoConstraints = false
    sv.showsVerticalScrollIndicator = false
    return sv
}()

let containerView: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = .white
    return view
}()

let calendarView: CalendarView = {
    let view = CalendarView() // contains a collectionView
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
}()

override func viewDidLoad() {
    super.viewDidLoad()

    createAnchors()
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: 1000)
}

func createAnchors() {

    view.addSubview(scrollView)
    scrollView.addSubview(containerView)

    scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
    scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
    scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
    scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true

    containerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
    containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
    containerView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true

    calendarView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: verticalPadding).isActive = true
    calendarView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 10).isActive = true
    calendarView.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -10).isActive = true
    calendarView.heightAnchor.constraint(equalToConstant: 290).isActive = true

    // textFields are added...
}

这是 CalenderView 的文件。还有 2 个我没有包含的文件,因为它们只是创建周和月的视图。

class CalendarView: UIView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, MonthViewDelegate {


let myCollectionView: UICollectionView = {
    let layout = UICollectionViewFlowLayout()
    layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)

    let myCollectionView=UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
    myCollectionView.showsHorizontalScrollIndicator = false
    myCollectionView.translatesAutoresizingMaskIntoConstraints=false
    myCollectionView.backgroundColor=UIColor.clear
    myCollectionView.allowsMultipleSelection=false
    myCollectionView.isScrollEnabled = false
    return myCollectionView
}()

var numOfDaysInMonth = [31,28,31,30,31,30,31,31,30,31,30,31]
var currentMonthIndex: Int = 0
var currentYear: Int = 0
var presentMonthIndex = 0
var presentYear = 0
var todaysDate = 0
var firstWeekDayOfMonth = 0   //(Sunday-Saturday 1-7)

override init(frame: CGRect) {
    super.init(frame: frame)

    initializeView()
}

func initializeView() {
    currentMonthIndex = Calendar.current.component(.month, from: Date())
    currentYear = Calendar.current.component(.year, from: Date())
    todaysDate = Calendar.current.component(.day, from: Date())
    firstWeekDayOfMonth=getFirstWeekDay()

    //for leap years, make february month of 29 days
    if currentMonthIndex == 2 && currentYear % 4 == 0 {
        numOfDaysInMonth[currentMonthIndex-1] = 29
    }
    //end

    presentMonthIndex=currentMonthIndex
    presentYear=currentYear

    setupViews()

    myCollectionView.delegate=self
    myCollectionView.dataSource=self
    myCollectionView.register(dateCVCell.self, forCellWithReuseIdentifier: "Cell")
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return numOfDaysInMonth[currentMonthIndex-1] + firstWeekDayOfMonth - 1
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell=collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! dateCVCell
    cell.backgroundColor=UIColor.clear
    if indexPath.item <= firstWeekDayOfMonth - 2 {
        cell.isHidden=true
    } else {
        let calcDate = indexPath.row-firstWeekDayOfMonth+2
        cell.isHidden=false
        cell.lbl.text="\(calcDate)"
        if calcDate < todaysDate && currentYear == presentYear && currentMonthIndex == presentMonthIndex {
            cell.isUserInteractionEnabled=false
            cell.lbl.textColor = UIColor.lightGray
        } else {
            cell.isUserInteractionEnabled=true
            cell.lbl.textColor = Style.activeCellLblColor
        }
    }
    return cell
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let cell=collectionView.cellForItem(at: indexPath)
    cell?.backgroundColor=Colors.darkRed
    let lbl = cell?.subviews[1] as! UILabel
    lbl.textColor=UIColor.white
}

func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
    let cell=collectionView.cellForItem(at: indexPath)
    cell?.backgroundColor=UIColor.clear
    let lbl = cell?.subviews[1] as! UILabel
    lbl.textColor = Style.activeCellLblColor
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let width = collectionView.frame.width/7 - 8
    let height: CGFloat = 30
    return CGSize(width: width, height: height)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return 8.0
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    return 8.0
}

func getFirstWeekDay() -> Int {
    let day = ("\(currentYear)-\(currentMonthIndex)-01".date?.firstDayOfTheMonth.weekday)!
    //return day == 7 ? 1 : day
    return day
}

func didChangeMonth(monthIndex: Int, year: Int) {
    currentMonthIndex=monthIndex+1
    currentYear = year

    //for leap year, make february month of 29 days
    if monthIndex == 1 {
        if currentYear % 4 == 0 {
            numOfDaysInMonth[monthIndex] = 29
        } else {
            numOfDaysInMonth[monthIndex] = 28
        }
    }
    //end

    firstWeekDayOfMonth=getFirstWeekDay()

    myCollectionView.reloadData()

    monthView.btnLeft.isEnabled = !(currentMonthIndex == presentMonthIndex && currentYear == presentYear)
}

func setupViews() {
    addSubview(monthView)
    monthView.topAnchor.constraint(equalTo: topAnchor).isActive=true
    monthView.leftAnchor.constraint(equalTo: leftAnchor).isActive=true
    monthView.rightAnchor.constraint(equalTo: rightAnchor).isActive=true
    monthView.heightAnchor.constraint(equalToConstant: 35).isActive=true
    monthView.delegate=self

    addSubview(weekdaysView)
    weekdaysView.topAnchor.constraint(equalTo: monthView.bottomAnchor).isActive=true
    weekdaysView.leftAnchor.constraint(equalTo: leftAnchor).isActive=true
    weekdaysView.rightAnchor.constraint(equalTo: rightAnchor).isActive=true
    weekdaysView.heightAnchor.constraint(equalToConstant: 30).isActive=true

    addSubview(myCollectionView)
    myCollectionView.topAnchor.constraint(equalTo: weekdaysView.bottomAnchor, constant: 0).isActive=true
    myCollectionView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0).isActive=true
    myCollectionView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0).isActive=true
    myCollectionView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive=true
}

let monthView: MonthView = {
    let v=MonthView()
    v.translatesAutoresizingMaskIntoConstraints=false
    return v
}()

let weekdaysView: WeekdaysView = {
    let v=WeekdaysView()
    v.translatesAutoresizingMaskIntoConstraints=false
    return v
}()

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
}

//get first day of the month
extension Date {
var weekday: Int {
    return Calendar.current.component(.weekday, from: self)
}
var firstDayOfTheMonth: Date {
    return Calendar.current.date(from: Calendar.current.dateComponents([.year,.month], from: self))!
}
}

//get date from string
extension String {
static var dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    return formatter
}()

var date: Date? {
    return String.dateFormatter.date(from: self)
}
}

仅用于 CalendarView 的视觉效果。日期 (1-30) 是一个 collectionView。

我从@zambrey 找到了 answer here by @Jaydeep and got the explanation

想法是告诉手势识别器不要吞掉触摸事件。为此,您需要将 singleTap 的 cancelsTouchesInView 属性 设置为 NO,默认情况下为 YES。

你想告诉 scrollView 不要吃掉所有的触摸事件,你可以通过向它添加一个点击手势识别器并设置点击的

singleTap.cancelsTouchesInView = false

我在将 CalenderView 嵌入 ScrollView 内的 UIView 的文件中添加了 tapGesture,而不是在具有 collectionView 的文件中。

override func viewDidLoad() {
    super.viewDidLoad()

    createAnchors()

    // add these 4 lines
    let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
    singleTap.cancelsTouchesInView = false
    singleTap.numberOfTapsRequired = 1
    scrollView.addGestureRecognizer(singleTap)
}

// add this target method. I didn't add any code whatsoever inside of it 
func handleTap(_ recognizer: UITapGestureRecognizer) {
  // I literally left this blank
}