Parcourir la source

feat: 优化嵌套 UIScrollView 的滚动处理逻辑

陈文艺 il y a 1 mois
Parent
commit
ec40748a0c

+ 0 - 2
Lanu.xcodeproj/project.pbxproj

@@ -90,8 +90,6 @@
 				Common/Views/Menu/LNBottomSheetMenu.swift,
 				Common/Views/Menu/LNCommonAlertView.swift,
 				Common/Views/NoMoreView/LNNoMoreDataView.swift,
-				Common/Views/ScrollView/LNNestedScrollView.swift,
-				Common/Views/ScrollView/LNNestedTableView.swift,
 				Common/Views/Selection/LNDatePickerPanel.swift,
 				Common/Views/Selection/LNHourRangePickerPanel.swift,
 				Common/Views/Selection/LNMultiSelectionPanel.swift,

+ 108 - 0
Lanu/Common/Extension/UIScrollView+Extension.swift

@@ -60,3 +60,111 @@ extension UIScrollView: LNKeyboardNotify {
         contentInset = originContentInset
     }
 }
+
+extension UIScrollView: UIGestureRecognizerDelegate {
+    func toBeNested() {
+        panGestureRecognizer.delegate = self
+        bounces = false
+    }
+    
+    // MARK: - UIGestureRecognizerDelegate
+    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
+        return true
+    }
+    
+    open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+        guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer,
+              let currentScrollView = panGesture.view as? UIScrollView else {
+            return true
+        }
+        
+        guard currentScrollView.isGestureWithinSelfBounds(panGesture) else {
+            return false // 不在范围内,直接忽略手势
+        }
+        
+        // 获取滑动方向(向上:y 负,向下:y 正)
+        let translation = panGesture.translation(in: currentScrollView)
+        let isScrollingUp = translation.y < 0 // 向上滑
+        let isScrollingDown = translation.y > 0 // 向下滑
+        
+        // 根据滑动方向判断当前 ScrollView 是否可以响应手势
+        if isScrollingUp {
+            return currentScrollView.checkOuterScrollViewsAtBottom(panGesture: panGesture)
+        } else if isScrollingDown {
+            return currentScrollView.checkInnerScrollViewsAtTop(panGesture: panGesture)
+        }
+        
+        return true
+    }
+    
+    // MARK: - UI 树遍历核心逻辑
+    /// 上滑:检查所有外层 ScrollView 是否都滚到底部
+    private func checkOuterScrollViewsAtBottom(panGesture: UIPanGestureRecognizer) -> Bool {
+        var currentSuperview = self.superview
+        // 向上遍历父视图,找所有外层 ScrollView
+        while let superview = currentSuperview {
+            if let outerScrollView = superview as? UIScrollView {
+                // 只要有一个外层没滚到底,当前 ScrollView 就不能响应
+                if !outerScrollView.isAtBottom() {
+                    return false
+                }
+            }
+            currentSuperview = superview.superview
+        }
+        // 所有外层都滚到底,当前可以响应
+        return !isAtBottom()
+    }
+    
+    /// 下滑:检查所有内层 ScrollView 是否都滚到顶部
+    private func checkInnerScrollViewsAtTop(panGesture: UIPanGestureRecognizer) -> Bool {
+        // 向下遍历子视图,找所有内层 ScrollView
+        // 递归遍历子视图的子视图(深层嵌套)
+        if findFirstNotAtTopScrollView(in: self, panGesture: panGesture) != nil {
+            return false
+        }
+        // 所有内层都滚到顶,当前可以响应
+        return !isAtTop()
+    }
+    
+    /// 递归查找子视图中的第一个 ScrollView(处理深层嵌套)
+    private func findFirstNotAtTopScrollView(
+        in view: UIView,
+        panGesture: UIPanGestureRecognizer) -> UIScrollView? {
+        for subview in view.subviews {
+            if let scrollView = subview as? UIScrollView,
+                scrollView.isGestureWithinSelfBounds(panGesture),
+                !scrollView.isAtTop() {
+                return scrollView
+            }
+            if let found = findFirstNotAtTopScrollView(in: subview, panGesture: panGesture) {
+                return found
+            }
+        }
+        return nil
+    }
+    
+    // MARK: - 滚动边界判断
+    private func isAtTop() -> Bool {
+        let topOffset = contentOffset.y + adjustedContentInset.top
+        return topOffset <= 1 // 允许1pt误差
+    }
+    
+    private func isAtBottom() -> Bool {
+        let contentHeight = contentSize.height + adjustedContentInset.bottom
+        let bottomOffset = contentOffset.y + bounds.height
+        return bottomOffset >= contentHeight - 1
+    }
+    
+    /// 判断手势触摸点是否在当前 ScrollView 的可视范围内
+    private func isGestureWithinSelfBounds(_ panGesture: UIPanGestureRecognizer) -> Bool {
+        // 1. 获取手势的触摸点(相对于当前 ScrollView)
+        let touchLocation = panGesture.location(in: self)
+        // 2. 转换为 ScrollView 的 bounds 坐标系(处理 contentOffset 和 contentInset)
+        let convertedPoint = CGPoint(
+            x: touchLocation.x - adjustedContentInset.left,
+            y: touchLocation.y - adjustedContentInset.top
+        )
+        // 3. 校验点是否在 ScrollView 的可视区域内(排除超出 bounds 的部分)
+        return bounds.contains(convertedPoint)
+    }
+}

+ 0 - 47
Lanu/Common/Views/ScrollView/LNNestedScrollView.swift

@@ -1,47 +0,0 @@
-//
-//  LNNestedScrollView.swift
-//  Lanu
-//
-//  Created by OneeChan on 2025/12/1.
-//
-
-import Foundation
-import UIKit
-
-
-class LNNestedScrollView: UIScrollView, UIGestureRecognizerDelegate {
-    override init(frame: CGRect) {
-        super.init(frame: frame)
-        self.panGestureRecognizer.delegate = self
-        self.bounces = false
-    }
-    
-    required init?(coder: NSCoder) {
-        super.init(coder: coder)
-        self.panGestureRecognizer.delegate = self
-        self.bounces = false
-    }
-    
-    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
-        guard let pan = gestureRecognizer as? UIPanGestureRecognizer
-        else { return false}
-        
-        let velocity = pan.velocity(in: self)
-        if velocity.y == 0 {
-            return true
-        }
-        
-        if let scrollView = pan.view as? UIScrollView {
-            if scrollView.contentOffset.y <= 0,
-               velocity.y > 0 {
-                return true
-            } else if Int(scrollView.contentOffset.y) >= Int(scrollView.contentSize.height - scrollView.bounds.height),
-                      velocity.y < 0 {
-                return true
-            }
-        }
-        
-        return false
-    }
-}
-

+ 0 - 46
Lanu/Common/Views/ScrollView/LNNestedTableView.swift

@@ -1,46 +0,0 @@
-//
-//  LNNestedTableView.swift
-//  Lanu
-//
-//  Created by OneeChan on 2025/12/1.
-//
-
-import Foundation
-import UIKit
-
-
-class LNNestedTableView: UITableView, UIGestureRecognizerDelegate {
-    override init(frame: CGRect, style: UITableView.Style) {
-        super.init(frame: frame, style: style)
-        self.panGestureRecognizer.delegate = self
-        self.bounces = false
-    }
-    
-    required init?(coder: NSCoder) {
-        super.init(coder: coder)
-        self.panGestureRecognizer.delegate = self
-        self.bounces = false
-    }
-    
-    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
-        guard let pan = gestureRecognizer as? UIPanGestureRecognizer
-        else { return false}
-        
-        let velocity = pan.velocity(in: self)
-        if velocity.y == 0 {
-            return true
-        }
-        
-        if let scrollView = pan.view as? UIScrollView {
-            if scrollView.contentOffset.y <= 0,
-               velocity.y > 0 {
-                return true
-            } else if Int(scrollView.contentOffset.y) >= Int(scrollView.contentSize.height - scrollView.bounds.height),
-                      velocity.y < 0 {
-                return true
-            }
-        }
-        
-        return false
-    }
-}

+ 4 - 0
Lanu/Common/Views/TextView/LNCommonTextView.swift

@@ -14,6 +14,7 @@ class LNCommonTextView: UIView {
     var maxInput = 0 {
         didSet {
             textViewDidChange(textView)
+            textLengthLabel.isHidden = maxInput == 0
         }
     }
     let placeholderLabel = UILabel()
@@ -46,6 +47,9 @@ class LNCommonTextView: UIView {
 
 extension LNCommonTextView: UITextViewDelegate {
     func textViewDidChange(_ textView: UITextView) {
+        placeholderLabel.isHidden = !textView.text.isEmpty
+        textLengthLabel.text = "\(textView.text.count)/\(maxInput)"
+        
         delegate?.textViewDidChange?(textView)
     }
     

+ 5 - 5
Lanu/Common/Views/VideoUpload/LNVideoCompressor.swift

@@ -190,11 +190,11 @@ extension LNVideoCompressor {
         let transform = videoTrack.preferredTransform
         var videoSize = naturalSize
         
-        // 处理旋转
-        if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 ||
-            transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 {
-            videoSize = CGSize(width: naturalSize.height, height: naturalSize.width)
-        }
+//        // 处理旋转
+//        if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 ||
+//            transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 {
+//            videoSize = CGSize(width: naturalSize.height, height: naturalSize.width)
+//        }
         
         // 限制最大分辨率(限制为720P)
         let maxDimension: CGFloat = 720

+ 0 - 4
Lanu/Views/Profile/Feed/LNProfileFeedItemCell.swift

@@ -434,10 +434,6 @@ extension LNProfileFeedItemCell {
     
     private func buildComment() -> UIView {
         commentView.uiColor = .text_4
-        commentView.onTap { [weak self] in
-            guard let self else { return }
-            toComment()
-        }
         
         return commentView
     }

+ 0 - 3
Lanu/Views/Profile/Profile/LNProfileInfosView.swift

@@ -31,8 +31,6 @@ class LNProfileInfosView: UIView {
     var progress: CGFloat = 0.0 {
         didSet {
             layer.cornerRadius = 20 * (1 - progress.bounded(min: 0, max: 1.0))
-            photoWall.isScrollEnable = progress >= 1.0
-            detailView.isScrollEnable = progress >= 1.0
         }
     }
     
@@ -163,7 +161,6 @@ extension LNProfileInfosView {
     
     private func buildPhotoWall() -> UIView {
         photoWall.delegate = self
-        photoWall.isScrollEnable = false
         
         return photoWall
     }

+ 2 - 9
Lanu/Views/Profile/Profile/LNProfilePhotoWall.swift

@@ -34,14 +34,6 @@ class LNProfilePhotoWall: UIView {
         }
     }
     
-    var isScrollEnable: Bool = false {
-        didSet {
-            tableViews.forEach {
-                $0.isScrollEnabled = isScrollEnable
-            }
-        }
-    }
-    
     weak var delegate: LNProfilePhotoWallDelegate?
     
     override init(frame: CGRect) {
@@ -191,7 +183,7 @@ extension LNProfilePhotoWall {
         }
         
         for _ in 0..<columnCount {
-            let tableView = LNNestedTableView()
+            let tableView = UITableView()
             tableView.separatorStyle = .none
             tableView.showsHorizontalScrollIndicator = false
             tableView.showsVerticalScrollIndicator = false
@@ -210,6 +202,7 @@ extension LNProfilePhotoWall {
                 })
                 contentHeight = (maxHeightItem?.contentInset.top ?? 0) + (maxHeightItem?.contentSize.height ?? 0)
             }.store(in: &bag)
+            tableView.toBeNested()
             stackView.addArrangedSubview(tableView)
             tableViews.append(tableView)
         }

+ 2 - 8
Lanu/Views/Profile/Profile/LNProfileUserDetailView.swift

@@ -23,7 +23,7 @@ class LNProfileUserDetailView: UIView {
         }
     }
     
-    private let tableView = LNNestedTableView(frame: .zero, style: .grouped)
+    private let tableView = UITableView(frame: .zero, style: .grouped)
     private let providers: [LNProfileBaseProvider] = [
         LNProfileUserInfoProvider(),
         LNProfileSkillListProvider(),
@@ -34,12 +34,6 @@ class LNProfileUserDetailView: UIView {
     private var curDetail: LNUserProfileVO?
     weak var delegate: LNProfileUserDetailViewDelegate?
     
-    var isScrollEnable: Bool = false {
-        didSet {
-            tableView.isScrollEnabled = isScrollEnable
-        }
-    }
-    
     override init(frame: CGRect) {
         super.init(frame: frame)
         
@@ -126,7 +120,6 @@ extension LNProfileUserDetailView {
     private func setupViews() {
         tableView.dataSource = self
         tableView.delegate = self
-        tableView.isScrollEnabled = false
         tableView.allowsSelection = false
         tableView.backgroundColor = .white
         tableView.showsVerticalScrollIndicator = false
@@ -139,6 +132,7 @@ extension LNProfileUserDetailView {
             contentHeight = newValue.height + tableView.contentInset.top + tableView.contentInset.bottom
         }.store(in: &bag)
         addSubview(tableView)
+        tableView.toBeNested()
         tableView.snp.makeConstraints { make in
             make.edges.equalToSuperview()
         }

+ 2 - 0
Lanu/Views/Profile/Profile/LNProfileVoiceBarView.swift

@@ -90,6 +90,8 @@ extension LNProfileVoiceBarView {
         
         statusIc.isUserInteractionEnabled = false
         statusIc.image = .icVoicePlay
+        statusIc.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+        statusIc.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
         addSubview(statusIc)
         statusIc.snp.makeConstraints { make in
             make.centerY.equalToSuperview()