关于我在 iOS 开发中用 Swift 实现的两个交互效果

Swift + UIView 实现通过透明度的判读进行事件穿透

在开发过程中会有这样一种场景:在一个页面A上有一个透明或者半透明的view B,希望在点击ViewB的透明或者半透明区域的时候,将点击事件透传给下层页面A。像下面这样,在蓝色的发布球动画播放播放结束后,有一部分发布球区域是透明的,此时我们希望这部分区域不响应发布球的点击事件,而是将手势透传到背后的背景,可能点击或者滑动。

废话不多说,swift实现如下:

swift 复制代码
@objcMembers
public class ViewB : UIView {
    // 如果点击点是透明的,则手势透传
    override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        return alphaOfPoint(point: point) != 0
    }
    
    func alphaOfPoint(point: CGPoint) -> CGFloat {
        return alphaOfPointFromLayer(point: point)
    }

    // 判断点击点的透明度
    func alphaOfPointFromLayer(point: CGPoint) -> CGFloat {
        var pixel = [UInt8](repeatElement(0, count: 4))
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
               context?.setBlendMode(.copy)
               context?.translateBy(x: -point.x, y: -point.y)
        if let context = context {
            layer.render(in: context)
        }
        let alpha = CGFloat(pixel[3]) / CGFloat(255.0)
        return alpha
    }
}

其中:

  • point(inside:with:)方法是 UIView 的一个覆盖方法,用于判断某个点是否在视图的可响应区域内。返回 true 表示该点在视图的边界内,返回false表示该点在视图的边界外。
  • 如果子视图不透明部分覆盖了 ViewB 的某个区域,那么在相应点上的透明度将是子视图的透明度。

Swift + UIScrollView 实现简易的多Tab横滑组件

在开发中,嵌套滚动视图(NestScrollView)是一种常见的UI组件,通常用于实现类似于Tab栏与内容页联动的效果。

在代码实现中,updateTabButtonSelection(selectedIndex:) 和 scrollViewDidScroll(_:) 是实现 Tab 栏与内容页联动 的核心方法,联动协作流程:

1.用户点击 Tab 按钮:

scss 复制代码
→ 触发 tabButtonTapped(_:)
→ 调用 updateTabButtonSelection(selectedIndex:)
→ 更新按钮状态并滚动 Tab 栏。
→ 手动设置 contentScrollView 的偏移量。

2.用户滑动内容页:

scss 复制代码
→ 触发 scrollViewDidScroll(_:)
→ 计算当前页面索引。
→ 超过阈值时更新 curSelectedIndex。
→ 调用 updateTabButtonSelection(selectedIndex:)
→ 同步 Tab 栏状态。

废话不多说,swift实现如下:

swift 复制代码
import UIKit

let mockData: [[String: String]] = [
    ["tabName": "tab0", "tabContent": "page0"],
    ["tabName": "tab1", "tabContent": "page1"],
    ["tabName": "tab2", "tabContent": "page2"],
    ["tabName": "tab3", "tabContent": "page3"],
    ["tabName": "tab4", "tabContent": "page4"],
    ["tabName": "tab5", "tabContent": "page5"],
    ["tabName": "tab6", "tabContent": "page6"]
]

public class NestScrollView: UIView {
    
    private let buttonWidth: CGFloat = 80 // 按钮的宽度
    private let tabBarHeight: CGFloat = 40 // TabBar的高度
    private let pageHeight: CGFloat = 200.0  // PageView的高度
    
    private var numberOfItems: Int = 0 // 页面的数量
    private var curSelectedIndex: Int = 0 // 跟踪当前选中的索引
    private let colors: [UIColor] = [.red, .black, .orange, .brown, .green, .cyan, .purple]

    private let containerView: UIView = UIView()
    private let tabBarScrollView: UIScrollView = UIScrollView()
    private var buttons: [UIButton] = []
    private let contentScrollView: UIScrollView = UIScrollView()
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
        numberOfItems = mockData.count
        setupView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
        tabBarScrollView.backgroundColor = .lightGray
        tabBarScrollView.showsHorizontalScrollIndicator = false
        addSubview(tabBarScrollView)
        
        contentScrollView.backgroundColor = .clear
        contentScrollView.isPagingEnabled = true
        contentScrollView.showsHorizontalScrollIndicator = false
        contentScrollView.delegate = self
//        contentScrollView.isScrollEnabled = false // 禁用滚动,只通过按钮点击切换页面
        addSubview(contentScrollView)
    }
    
    override public func layoutSubviews() {
        super.layoutSubviews()
        containerView.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height);
        layoutTabBar()
        layoutContentScrollView()
    }
    
    private func layoutTabBar() {
        tabBarScrollView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: tabBarHeight)
        tabBarScrollView.contentSize = CGSize(width: buttonWidth * CGFloat(numberOfItems), height: tabBarHeight)
        
        // 移除已存在的子视图
        buttons.forEach { $0.removeFromSuperview() }
        buttons.removeAll()
        
        // 添加每一个 tabView
        for (index, data) in mockData.enumerated() {
            let button = UIButton(type: .system)
            button.setTitle(data["tabName"], for: .normal)
            button.tag = index
            button.addTarget(self, action: #selector(tabButtonTapped(_:)), for: .touchUpInside)
            button.frame = CGRect(x: buttonWidth * CGFloat(index), y: 0, width: buttonWidth, height: tabBarHeight)
            buttons.append(button)
            tabBarScrollView.addSubview(button)
        }
        
        // 初始化选中
        updateTabButtonSelection(selectedIndex: 0)
    }
    
    private func layoutContentScrollView() {
        // 下面内容区的布局
        let pageWidth = containerView.frame.width

        contentScrollView.frame = CGRect(x: 0.0, y: tabBarHeight, width: pageWidth, height: pageHeight)
        contentScrollView.contentSize = CGSize(width: CGFloat(numberOfItems) * pageWidth, height: pageHeight)

        // 移除已存在的子视图
        contentScrollView.subviews.forEach { $0.removeFromSuperview() }
        
        // 添加每一个 pageView
        for (index, data) in mockData.enumerated() {
            let pageView = UIView(frame: CGRect(x: CGFloat(index) * pageWidth, y: 0, width: pageWidth, height: pageHeight))
            let colorIndex = index % colors.count // 颜色循环
            pageView.backgroundColor = colors[colorIndex]
            
            // 添加一个标签到每个 pageView
            let label = UILabel(frame: pageView.bounds)
            label.text = data["tabContent"]
            label.textAlignment = .center
            label.textColor = .white
            label.font = UIFont.boldSystemFont(ofSize: 24)
            
            pageView.addSubview(label)
            
            contentScrollView.addSubview(pageView)
        }
    }

    
    @objc private func tabButtonTapped(_ sender: UIButton) {
        curSelectedIndex = sender.tag
        let offset = CGPoint(x: CGFloat(curSelectedIndex) * contentScrollView.bounds.width, y: 0)
        contentScrollView.setContentOffset(offset, animated: false)
        updateTabButtonSelection(selectedIndex: curSelectedIndex)
    }
    
    private func updateTabButtonSelection(selectedIndex: Int) {
        for (index, button) in buttons.enumerated() {
            button.isSelected = (index == selectedIndex)
            button.setTitleColor(button.isSelected ? .white : .black, for: .normal)
            button.backgroundColor = button.isSelected ? .blue : .clear
        }
        
        let selectedButton = buttons[selectedIndex]
        let buttonFrame = selectedButton.frame
        let buttonLeftX = buttonFrame.minX
        let buttonRightX = buttonFrame.maxX
        let scrollOffset = tabBarScrollView.contentOffset.x
        let scrollWidth = tabBarScrollView.bounds.width
        var newOffset: CGFloat = scrollOffset
        
        if buttonLeftX < scrollOffset {
            newOffset = buttonLeftX
        } else if buttonRightX > scrollOffset + scrollWidth {
            newOffset = buttonRightX - scrollWidth
        }
        let maxOffset = tabBarScrollView.contentSize.width - scrollWidth
        newOffset = max(0, min(newOffset, maxOffset))
        tabBarScrollView.setContentOffset(CGPoint(x: newOffset, y: 0), animated: true)
    }
}

extension NestScrollView: UIScrollViewDelegate {
    
    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // 根据当前页面计算边界
        let pageWidth = containerView.frame.width
        let scrollThreshold = pageWidth / 2.0
        
        // 检测滚动距离,只有当距离超过阈值时才更新页面
        let contentOffset = scrollView.contentOffset.x
        let pageIndex = Int((contentOffset + (pageWidth / 2)) / pageWidth) // 计算出当前页索引
        let scrollDelta = contentOffset - CGFloat(curSelectedIndex) * pageWidth
        
        if abs(scrollDelta) > scrollThreshold && pageIndex != curSelectedIndex && pageIndex >= 0 && pageIndex < numberOfItems {
            curSelectedIndex = pageIndex
            updateTabButtonSelection(selectedIndex: curSelectedIndex)
        }
    }
}
相关推荐
蓝婷儿5 小时前
前端面试每日三题 - Day 32
前端·面试·职场和发展
WDeLiang6 小时前
Flutter - UIKit开发相关指南 - 导航
flutter·ios·dart
星空寻流年6 小时前
CSS3(BFC)
前端·microsoft·css3
九月TTS6 小时前
开源分享:TTS-Web-Vue系列:Vue3实现固定顶部与吸顶模式组件
前端·vue.js·开源
CodeCraft Studio7 小时前
数据透视表控件DHTMLX Pivot v2.1发布,新增HTML 模板、增强样式等多个功能
前端·javascript·ui·甘特图
一把年纪学编程7 小时前
【牛马技巧】word统计每一段的字数接近“字数统计”
前端·数据库·word
llc的足迹7 小时前
el-menu 折叠后小箭头不会消失
前端·javascript·vue.js
九月TTS7 小时前
TTS-Web-Vue系列:移动端侧边栏与响应式布局深度优化
前端·javascript·vue.js
Johnstons7 小时前
AnaTraf:深度解析网络性能分析(NPM)
前端·网络·安全·web安全·npm·网络流量监控·网络流量分析
文件夹__iOS8 小时前
深入浅出 iOS 对象模型:isa 指针 与 Swift Metadata
ios·swift