iOS界面操作指引的实现参考

       当app 新增了功能为了更好地使用户熟悉和使用,需要使用操作指引来进行介绍和引导。效果参考图如下:

实现该功能有多个思路,本文中主要介绍的是将遮罩、镂空区域以及下一步、跳过等元素动态处理而非切整张图的形式来实现,优点是:减少包的大小以及使用代码动态适配定位需要指引的栏目,支持多界面滚动定位指引。缺点,如果界面很长,需要指引的栏目在可见区域之外,此时处理会稍微麻烦(详见文中代码注释,此处有很大改进空间)。

1、主体实现参考代码:

import UIKit
import YYImage
import RxSwift
import SnapKit

/// 操作指引视图
class DrmbbView: UIView {
    
    /// 操作事件集
    private var arrActions:[DrmbbModel]
    
    /// 描述图片的偏移量
    private var oy:CGFloat
    
    /// 页面高度过高,超出一个屏幕高度需要使用滚动来定位指示
    private weak var contentScrollView:UIScrollView?
    
    /// 跳过事件回调
    var jumpActionBlock:(()->Void)?
    
    /// 当前引导完成回调
    var guideFinishBlock:(()->Void)?
    
    
    //MARK: - override
    init(frame:CGRect,
         andActions _a:[DrmbbModel],
         withScrollerView _scv:UIScrollView?,
         andOffsetY _oy:CGFloat = 0) {
        self.arrActions = _a
        self.oy = _oy
        self.contentScrollView = _scv
        
        super.init(frame: frame)
        self.initView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func updateConstraints() {
        
        if self.subviews.contains(self.btnJump) {
            self.btnJump.snp.makeConstraints { make in
                make.right.equalTo(-16)
                make.width.equalTo(72)
                make.height.equalTo(32)
                make.top.equalTo(Setting.shareInstance.K_APP_SAFETY_NAV_HEIGHT + (44 - 32) * 0.5)
            }
        }
        
        super.updateConstraints()
    }
    
    private func initView(){
        self.backgroundColor = .clear
        self.addSubview(self.btnJump)
        self.showNext()
        
        setNeedsUpdateConstraints()
    }
    
    
    deinit {
        self.deInit()
        print("\(self.className()) 已销毁")
    }
    
    
    //MARK: - lazy load
    private lazy var disposeBag = DisposeBag()
    
    private lazy var showIndex:Int = 0
    private lazy var cornerRadius:CGFloat = 0
    
    /// 跳过
    private lazy var btnJump:UIButton = {[unowned self] in
        let _btn = BaseView.createBtn(rect: .zero,
                                      strTitle: "跳过",
                                      titleColor: .white,
                                      txtFont: UIFont.systemFont(ofSize: 14, weight: .regular),
                                      image: nil,
                                      backgroundColor: UIColor.init().colorFromHexInt(hex: 0xCF5F42),
                                      borderColor: nil,
                                      cornerRadius: 16,
                                      isRadius: true,
                                      backgroundImage: nil,
                                      borderWidth: nil)
        
        _btn.rx.tap.subscribe {[weak self] (_:Event<Void>) in
            guard let self = self else { return }
            self.jumpActionBlock?()
            self.deInit()
        }.disposed(by: self.disposeBag)
        
        return _btn
    }()
    
    /// 遮罩区域
    private lazy var fillLayer:CAShapeLayer = {[unowned self] in
        let _layer = CAShapeLayer.init()
        _layer.fillRule = .evenOdd
        _layer.fillColor = UIColor.black.cgColor
        _layer.opacity = 0.6
        
        return _layer
    }()
    
    /// 描述内容图片(下一步/知道了)
    private lazy var btnContentImage:UIButton = {[unowned self] in
        let _btn = BaseView.createBtn(rect: .zero,
                                      strTitle: nil,
                                      titleColor: nil,
                                      txtFont: nil,
                                      image: nil,
                                      backgroundColor: nil,
                                      borderColor: nil,
                                      cornerRadius: 0,
                                      isRadius: false,
                                      backgroundImage: nil,
                                      borderWidth: nil)
        
        _btn.showsTouchWhenHighlighted = false
        _btn.rx.tap.subscribe {[weak self] (_:Event<Void>) in
            guard let self = self else { return }
            self.showIndex = self.btnContentImage.tag + 1
            self.showNext()
        }.disposed(by: self.disposeBag)
        
        return _btn
    }()
}


//MARK: -
extension DrmbbView {
    
    /// 显示下一步
    private func showNext(){
        if self.showIndex < self.arrActions.count {
            let _model = self.arrActions[self.showIndex]
            
            //当前栏目的镂空区域
            let _transparentRoundedRectPath = UIBezierPath.init(roundedRect: _model.contentFrame,
                                                                cornerRadius: _model.contentCornerRadius)
            
            //[S]外层遮罩区域
            let _bezierPath = UIBezierPath.init(rect: self.frame)
            _bezierPath.append(_transparentRoundedRectPath)
            _bezierPath.usesEvenOddFillRule = true
            
            self.fillLayer.path = _bezierPath.cgPath
            if self.layer.sublayers == nil || self.layer.sublayers?.contains(self.fillLayer) == false {
                self.layer.insertSublayer(self.fillLayer, at: 0)
            }
            //[E]
            
            //[S]设置描述图片
            if let _img:YYImage = YYImage.init(named: _model.descriptionImageName) {
                self.btnContentImage.setBackgroundImage(_img, for: .normal)
                self.btnContentImage.tag = self.showIndex
                
                if self.subviews.contains(self.btnContentImage) == false {
                    self.addSubview(self.btnContentImage)
                }
                
                var _offsetTop = _model.contentFrame.origin.y + _model.contentFrame.size.height
                if !_model.isFacedown {
                    _offsetTop -= _model.contentFrame.size.height
                    _offsetTop -= _img.size.height
                    if !UIDevice.current.isiPhoneX() {
                        _offsetTop -= Setting.shareInstance.K_APP_NAVIGATION_BAR_HEIGHT
                    }
                }
                else{
                    _offsetTop -= self.oy
                }
                
                self.cornerRadius = _model.contentCornerRadius
                self.scroller(ContentOffset: _model.contentFrame)
                
                self.btnContentImage.snp.remakeConstraints { make in
                    make.width.equalTo(_img.size.width)
                    make.height.equalTo(_img.size.height)
                    make.centerX.equalToSuperview()
                    make.top.equalTo(_offsetTop)
                }
            }
            else{
                self.btnContentImage.removeFromSuperview()
                self.cornerRadius = _model.contentCornerRadius
                self.scroller(ContentOffset: _model.contentFrame)
            }
            //[E]
        }
        else {
            self.guideFinishBlock?()
            self.deInit()
        }
    }
    
    
    /// 销毁
    private func deInit(){
        self.arrActions.removeAll()
        self.jumpActionBlock = nil
        self.guideFinishBlock = nil
        self.contentScrollView = nil
        self.fillLayer.removeAllAnimations()
        self.fillLayer.removeFromSuperlayer()
        
        self.subviews.forEach {
            $0.removeAllSubviews()
            $0.removeFromSuperview()
            $0.removeObserverBlocks()
            $0.removeAssociatedValues()
        }
        
        self.removeFromSuperview()
        self.removeObserverBlocks()
        self.removeAssociatedValues()
    }
    
    
    /// 当前镂空区域,如果不在可见区域,则要重定位
    private func scroller(ContentOffset _offset:CGRect){
        let _oy = UIDevice.current.isSmallDevice() ? _offset.origin.y : _offset.size.height + _offset.origin.y
        
        //[S] 显示的栏目不存在当前可见区域
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
            var _vh:CGFloat? = self.contentScrollView?.frame.size.height
            if #available(iOS 12.0, *) {
                _vh = self.contentScrollView?.visibleSize.height
            }
            
            if _vh != nil && _vh! + Setting.shareInstance.K_APP_NAVIGATION_BAR_HEIGHT < _oy,
               let _csize = self.contentScrollView?.contentSize,
               let _vh = self.contentScrollView?.bounds.size.height {
                //[S] 滚到对应栏目
                var _point = CGPoint.init(x: 0, y: _csize.height - _vh)
                let _y = _point.y - _offset.size.height - (UIDevice.current.isiPhoneX() ? 0 : 286)
                if _y > Setting.shareInstance.K_APP_HEIGHT - Setting.shareInstance.K_APP_TABBAR_HEIGHT {
                    _point.y -= _offset.size.height - (UIDevice.current.isiPhoneX() ? 123 : 71)
                }
                self.contentScrollView?.setContentOffset(_point, animated: true)
                //[E]
                
                //[S] 重新定位描述图片位置
                _point = CGPoint.init(x: _offset.size.width, y: _offset.size.height)
                var _offsetTop = self.convert(_point, to: self.contentScrollView!)
                if UIDevice.current.isiPhoneX(),
                   let _setPoint = self.contentScrollView?.convert(_point, to: self) {
                    _offsetTop = _setPoint
                }
                
                var _newRect = _offset
                _newRect.origin.y = _offsetTop.y
                if self.subviews.contains(self.btnContentImage) {
                    self.btnContentImage.snp.updateConstraints { make in
                        make.top.equalTo(_newRect.origin.y + _newRect.size.height)
                    }
                }
                //[E]
                
                //[S]更新镂空区域位置
                let _transparentRoundedRectPath = UIBezierPath.init(roundedRect: _newRect,
                                                                    cornerRadius: self.cornerRadius)
                
                let _bezierPath = UIBezierPath.init(rect: self.frame)
                _bezierPath.append(_transparentRoundedRectPath)
                _bezierPath.usesEvenOddFillRule = true
                
                self.fillLayer.path = _bezierPath.cgPath
                //[E]
            }
        }
        //[E]
    }
}

模型文件:

import UIKit

/// 操作指引模型
struct DrmbbModel: Codable {
    /// 镂空区域位置
    let contentFrame:CGRect
    /// 镂空区域的圆角
    let contentCornerRadius:CGFloat
    /// 描述内容的图片
    let descriptionImageName:String
    /// 描述内容的图片朝向(true 朝下,false 朝上)
    let isFacedown:Bool
    
    ///防止服务端下发字段多余当前字段,而无法匹配解析
    struct DrmbbModel : Decodable {
        let contentFrame:CGRect
        let contentCornerRadius:CGFloat
        let descriptionImageName:String
        let isFacedown:Bool
    }
}

 2、调用方法:

//MARK: 操作指引视图
    private lazy var drmbbView:DrmbbView = {[unowned self] in
        let _rect:CGRect = CGRect.init(x: 0, y: 0,
                                       width: Setting.shareInstance.K_APP_WIDTH,
                                       height: Setting.shareInstance.K_APP_HEIGHT - Setting.shareInstance.K_APP_TABBAR_HEIGHT)
        
        let _arrAction:[DrmbbModel] = [
            DrmbbModel.init(contentFrame: CGRect.init(origin: CGPoint.init(x: 25, y: Setting.shareInstance.K_APP_NAVIGATION_BAR_HEIGHT + 90),size: CGSize.init(width: Setting.shareInstance.K_APP_WIDTH - 50, height: 71)),
                            contentCornerRadius: 12.5,
                            descriptionImageName: "trade_01.png",
                            isFacedown: true),
            DrmbbModel.init(contentFrame: CGRect.init(origin: CGPoint.init(x: 25, y: Setting.shareInstance.K_APP_NAVIGATION_BAR_HEIGHT + 150),size: CGSize.init(width: Setting.shareInstance.K_APP_WIDTH - 50, height: 63)),
                            contentCornerRadius: 12.5,
                            descriptionImageName: "trade_02.png",
                            isFacedown: true),
            DrmbbModel.init(contentFrame: CGRect.init(origin: CGPoint.init(x: 13, y: Setting.shareInstance.K_APP_NAVIGATION_BAR_HEIGHT + 225),size: CGSize.init(width: Setting.shareInstance.K_APP_WIDTH - 26, height: 154)),
                            contentCornerRadius: 12.5,
                            descriptionImageName: "trade_03.png",
                            isFacedown: true),
            DrmbbModel.init(contentFrame: CGRect.init(origin: CGPoint.init(x: 13, y: Setting.shareInstance.K_APP_NAVIGATION_BAR_HEIGHT + 883),size: CGSize.init(width: Setting.shareInstance.K_APP_WIDTH - 26, height: 206)),
                            contentCornerRadius: 12.5,
                            descriptionImageName: "trade_04.png",
                            isFacedown: true),
        ]
        
        let _v = DrmbbView.init(frame: _rect,
                                andActions: _arrAction,
                                withScrollerView: self.mainView.listCollectionView,
                                andOffsetY: 0)
        
        _v.jumpActionBlock = {[weak self] in
            UserDefaults.standard.set(true, forKey: Key.shareInstance.trade_action_guide)
            self?.drmbbView.removeFromSuperview()
            self?.drmbbView.removeObserverBlocks()
            self?.drmbbView.removeAssociatedValues()
        }
        
        _v.guideFinishBlock = {[weak self] in
            self?.drmbbView.jumpActionBlock?()
        }
        
        return _v
    }()


//在VC 的 viewDidLoad 中添加调用
self.view.addSubview(self.drmbbView)

经模拟器上不同设备及系统(iPhoneSE一代、iPhoneXs、iPhoneXR、iPhone6/7/8Plus)上测试,效果有出入,基本偏差在可接受范围内,有其他高见欢迎留言拍砖

猜你喜欢

转载自blog.csdn.net/yimiyuangguang/article/details/126537733