swift 可拖拽的浮标
import UIKit
import RxSwift
import RxCocoa
import YPSVG
/// 关联的拖拽浮标的key
fileprivate var DragBuoyKey = "DragBuoyKey"
fileprivate var DelBuoyKey = "DelBuoyKey"
fileprivate var HiddenBuoyKey = "HiddenBuoyKey"
extension UIViewController {
/// 拖拽类型
enum DragBuoyType: String {
/// 首页(招工大列表)
case MAIN_FIND_WORKER_LIST = "MAIN_FIND_WORKER_LIST"
/// 招工详情页
case FIND_WORKER_DETAIL = "FIND_WORKER_DETAIL"
/// 我的招工详情
case MY_FIND_WORKER_DETAIL = "MY_FIND_WORKER_DETAIL"
/// 我的招工列表
case MY_FIND_WORKER_LIST = "MY_FIND_WORKER_LIST"
/// 找活列表页
case MAIN_MY_FIND_JOB_LIST = "MAIN_MY_FIND_JOB_LIST"
/// 找活详情页
case FIND_JOB_DETAIL = "FIND_JOB_DETAIL"
/// 我的找活名片
case MY_RESUME = "MY_RESUME"
/// 消息
case MAIN_MESSAGES = "MAIN_MESSAGES"
/// 会员中心
case MAIN_USER_CENTER = "MAIN_USER_CENTER"
/// 关于鱼泡
case aboutAppForFaceBook = "ABOUT_APP_FOR_FACEBOOK"
}
/// 动态关联一个DisposeBag 每次滚动的时候会取消定时器
private var hiddenBuoy: DisposeBag{
set{
objc_setAssociatedObject(self, &HiddenBuoyKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get{
if let items = objc_getAssociatedObject(self, &HiddenBuoyKey) as? DisposeBag{
return items
}else{
let tmp = DisposeBag()
objc_setAssociatedObject(self, &HiddenBuoyKey, tmp, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return tmp
}
}
}
/// 动态管理一个添加的数组
fileprivate var showItems: NSMutableArray{
if let items = objc_getAssociatedObject(self, &DragBuoyKey) as? NSMutableArray{
return items
}
let tmp : NSMutableArray = .init()
objc_setAssociatedObject(self, &DragBuoyKey, tmp, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return tmp
}
fileprivate var delItems: NSMutableArray{
if let items = objc_getAssociatedObject(self, &DelBuoyKey) as? NSMutableArray{
return items
}
let tmp : NSMutableArray = .init()
objc_setAssociatedObject(self, &DelBuoyKey, tmp, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return tmp
}
/// 获取浮标数据
/// - Parameters:
/// - entranceString: 入口url -entranceString后台配置的一致
/// - isIndex: isIndex在首页 1便是 0-不是
/// - jobId:弹窗的-jobId
/// - isReviewed: 找活名片是不是审核通过了 - 需要单独处理的
func getBuoyData(entranceString: DragBuoyType,isIndex: Int = 0,jobId: String = "",isReviewed: Bool = true){
if IsActive(){ // 有的浮标不通过线上审核也需要显示的
YPGeneralInterface.getBuoy(entrance: entranceString.rawValue,isIndex: isIndex).request(model: YPResponseModel<YPBuoyArrayModel>.self).bind{ [weak self] res in
guard let weakSelf = self else {return}
if res.isOk {
if let model = res.data?.lists {
var delArray: [Int: YPBuoyListModel] = [:]
model.forEach { buoyModel in
if let firstModel = buoyModel.list?.first {
if firstModel.landingPageUrl == YPNavigationPath.Resume.cardPreview.rawValue.navigatorPath && isReviewed == false {
return
}
firstModel.homeIndex = isIndex
firstModel.jobId = jobId
delArray[buoyModel.position ?? 1] = firstModel
}
}
weakSelf.buoyAdditem(listArray: delArray)
}
}
}.disposed(by: rx.disposeBag)
}
}
/// 创建浮标
private func buoyAdditem(listArray:[Int: YPBuoyListModel]){
/// 移除items
showItems.forEach { item in
(item as? YPBuoyView)?.removeFromSuperview()
}
showItems.removeAllObjects()
listArray.forEach { position,listModel in
let id = listModel.id ?? 0
if !YPBuoyViewManager.manager.addBuoyView(vc: self, id: id) { return }
/// 添加浮标view
let view = YPBuoyView(model: listModel)
if let rect = YPBuoyViewManager.manager.getBuoyRect(vc: self, id: id){
view.frame = rect
}else{
var tmpHeight = YPScreen.screen_h - YPScreen.safe_bottom
if let controller = UIViewController.getCurrent, controller.navigationController?.viewControllers.count == 1{
tmpHeight -= 49
}
//距离底部100
tmpHeight -= 90.scale
if listModel.entranceUrl == DragBuoyType.FIND_WORKER_DETAIL.rawValue || listModel.entranceUrl == DragBuoyType.MY_FIND_WORKER_DETAIL.rawValue {/// 如果是招工详情界面需要留出置顶按钮位置
tmpHeight -= 55.scale
}else if listModel.entranceUrl == DragBuoyType.MAIN_FIND_WORKER_LIST.rawValue {
tmpHeight += 30.scale
}
/// 间距
let offest: CGFloat = 8.scale
let num: CGFloat = 3-CGFloat(position)
let viewH: CGFloat = view.mj_h+offest
let positionH: CGFloat = num * viewH
let totalH = tmpHeight-TabBarHeight-positionH
view.mj_y = totalH
// - 80.scale
/// 根据id-保存frame位置
YPBuoyViewManager.manager.addBuoyRect(vc: self, id: id, rect: view.frame)
}
view.closeButton.rx.tap().driverJustComplete {[weak self] _ in
guard let weakSelf = self else {return}
weakSelf.closeAct(view)
}.disposed(by: view.rx.disposeBag)
//初始化点击手势
let tap = UITapGestureRecognizer.init()
//绑定
tap.rx.event.filter{$0.isEnabled}.debounce(.microseconds(500), scheduler: MainScheduler.asyncInstance).bind{[weak self]_ in
guard let weakSelf = self else {return}
weakSelf.iconClick(listModel: listModel,position: position)
}.disposed(by: rx.disposeBag)
//添加点击手势
view.iconView.addGestureRecognizer(tap)
showItems.add(view)
self.view.addSubview(view)
}
}
/// 关闭按钮的点击事件
@objc private func closeAct(_ sender : YPBuoyView){
showItems.remove(sender)
sender.removeFromSuperview()
delItems.add(sender.model.type ?? 0)
YPBuoyViewManager.manager.delete(vc: self, id: sender.model.id ?? 0)
}
/// 浮标的点击事件
func iconClick(listModel: YPBuoyListModel,position: Int){
if listModel.type == 1 { /// 1-普通类型 有三种情况的处理
nomalCommonType(listModel: listModel)
}else if listModel.type == 2 { /// 2-大转盘类型 -直接跳大转盘
didPlaylucky()
}else if listModel.type == 3 { /// 3-分享类型
bouncedProcessing(listModel: listModel) // normal - text - photo
}else if listModel.type == 4 { /// 4-首页-分享送积分类型
getIntegarl(homeIndex: listModel.homeIndex ?? 1)
}else if listModel.type == 5{ /// 免费送好礼
navigatorEx?.push(YPNavigationPath.Member.Gift.rawValue.navigatorPath)
}else{ /// 最后其他的处理为-普通类型-
nomalCommonType(listModel: listModel)
}
}
// 1-普通类型处理 -分三种情况
func nomalCommonType(listModel: YPBuoyListModel){
if listModel.jumpType == 1 {//1-跳转H5页面
let webVC = BaseWebViewController(navigationType: .blueStyle)
webVC.title = listModel.title
webVC.fullURL = listModel.landingPageUrl
navigationController?.pushViewController(webVC, animated: true)
}else if listModel.jumpType == 2 { // 2-跳转内部URL
/// 是小说的跳转需要做点击的统计-需单独处理
if listModel.landingPageUrl == YPNavigationPath.Member.story.rawValue.navigatorPath {
YPGeneralInterface.novelStatistical(btnIDType: "100004").request(model: YPResponseModel<String>.self).bind{ res in
print(res)
}.disposed(by: rx.disposeBag)
}
navigatorEx?.push("\(listModel.landingPageUrl ?? "")")
}else if listModel.jumpType == 3{ // 3-跳转小程序
if WXApi.isWXAppInstalled() {
let req = WXLaunchMiniProgramReq()
/// 拉起的小程序的username-原始ID
req.userName = listModel.mini ?? ""
/// 拉起小程序页面的可带参路径,不填默认拉起小程序首页
req.path = listModel.landingPageUrl
switch GetBASEURL() {
case BaseUrlTest: // 测试站
req.miniProgramType = .preview
case BaseUrlPre: // 预发布
req.miniProgramType = .preview
case BaseUrlReleasePre:/// 预发布正式站
req.miniProgramType = .preview
case BaseUrlRelease:// 正式站
req.miniProgramType = .release
default:
break
}
WXApi.send(req) { _ in }
}
}
}
/// 弹框处理
func bouncedProcessing(listModel: YPBuoyListModel){
let page = "\(self.classForCoder)"
if listModel.entranceUrl == DragBuoyType.FIND_WORKER_DETAIL.rawValue { /// 如果别人的是招工详情页
let jobId: Int = Int(listModel.jobId ?? "0") ?? 0
YPShareEntCustorm.init(page, .broadside, .job, jobId).doshare()
}else if listModel.entranceUrl == DragBuoyType.MY_FIND_WORKER_DETAIL.rawValue { // 分享到群的弹框
let jobId: Int = Int(listModel.jobId ?? "0") ?? 0
YPShareEntCustorm.init(page, .broadsideme, .job, jobId).doshare()
}else {
if let resumeVc = self as? YPResumeDetailVC {
let detailId: Int = Int(resumeVc.resumeID ?? "0") ?? 0
YPShareEntCustorm.init(page, .buoyShare, .resume, detailId).doshare()
}else if let recruitVc = self as? RecruitDetailVC{
let detailId: Int = recruitVc.detailDataM.value?.id ?? 0
YPShareEntCustorm.init(page, .buoyShare, .job, detailId).doshare()
}else{
YPShareEntCustorm.init(page, .buoyShare).doshare()
}
}
}
/// 首页的分享赚积分逻辑
func getIntegarl(homeIndex: Int) {
let share = YPShareEntCustorm.init("\(self.classForCoder)", .buoyShareGetIntegral)
share.willShare = {[weak self] model in
guard let weakSelf = self else { return }
let turntableType = model.turntableType ?? "2"
return YPIndexAPI.queryIncentiveShare.request(model: YPCodeMsgRespModel<String>.self).filter{$0.isOk}.flatMap{ _ -> Observable<FastResumeAlertView.Action> in
let msg = NSMutableAttributedString()
let left: NSMutableAttributedString?
let right: NSMutableAttributedString?
if turntableType == "2" {
msg.append("获得".axc_attributedStr(color: "#333333", font: 14))
msg.append("1".axc_attributedStr(color: UIColor.red, font: 14))
msg.append("临时积分。\n玩大转盘免费获得更多积分,去赚积分?".axc_attributedStr(color: "#333333", font: 14))
left = "知道了".axc_attributedStr(color: ColorLightGray, font: 14)
right = "去玩大转盘".axc_attributedStr(color: "#333333", font: 14)
}else {
msg.append("获得".axc_attributedStr(color: "#333333", font: 14))
msg.append("1".axc_attributedStr(color: UIColor.red, font: 14))
msg.append("临时积分".axc_attributedStr(color: "#333333", font: 14))
left = "知道了".axc_attributedStr(color: ColorLightGray, font: 14)
right = nil
}
return FastResumeAlertView.show(title: "分享成功", msg: msg, left: left, right: right)
}.bind{[weak self] action in
guard let weakSelf = self else {return}
// 重新请求下判断是哪个类型的浮标 homeIndex:1-首页
weakSelf.getBuoyData(entranceString: .MAIN_FIND_WORKER_LIST, isIndex: homeIndex)
//跳转前 请求一次分享统计
YPReportAPI.reportShareAction(type: "7").request.subscribe{ _ in }.disposed(by: weakSelf.rx.disposeBag)
//点击右键玩大转盘
if action == .right{
weakSelf.didPlaylucky()
}
}.disposed(by: weakSelf.rx.disposeBag)
}
share.doshare()
}
// 大转盘
func didPlaylucky(){
navigatorEx?.push(YPNavigationPath.Member.Playlucky.rawValue.navigatorPath)
}
}
class YPBuoyViewManager: NSObject {
/// 单例对象
static let manager = YPBuoyViewManager()
/// 视图池
var suspendedList: [YPBuoyView] = []
/// 存放的控制器类型
private var buoyVcTypeList: [String: [Int]] = [:]
/// 二维数组 : 1.控制器 2.id
private var locationList: [String: [Int:CGRect]] = [:]
override private init() {
super.init()
}
/// 添加浮标
func addBuoyView(vc: UIViewController,id: Int) -> Bool{
let className = "\(vc.classForCoder)"
if let tmpArray = buoyVcTypeList[className]{
if tmpArray.firstIndex(of: id) != nil{
return false
}
}
return true
}
func addBuoyRect(vc: UIViewController,id: Int,rect: CGRect){
let className = "\(vc.classForCoder)"
var tmpArray = locationList[className] ?? [:]
tmpArray[id] = rect
locationList[className] = tmpArray
}
func getBuoyRect(vc: UIViewController,id: Int)->CGRect?{
let className = "\(vc.classForCoder)"
let tmpArray = locationList[className]
return tmpArray?[id]
}
/// 删除浮标
func delete(vc: UIViewController,id: Int) {
let className = "\(vc.classForCoder)"
var tmpArray = buoyVcTypeList[className] ?? []
tmpArray.append(id)
buoyVcTypeList[className] = tmpArray
locationList[className]?.removeValue(forKey: id)
}
}
/// MARK: - 拖拽的浮标
class YPBuoyView: View{
/// 是不是能拖曳,默认为YES
var dragEnable: Bool = true
/// 起点
var startPoint: CGPoint = .zero
/// 拖拽的范围-指定父视图-拖出父视图后无法点击
var freeRect: CGRect = .zero
/// 是否自动黏贴边界,默认为true 不自动黏贴边界
var isKeepBounds: Bool = true
/// 记录model
let model: YPBuoyListModel
/// 图片大小
let imgW: CGFloat
let imgH: CGFloat
/// 关闭按钮
lazy var closeButton: YPSVGView = {
let button = YPSVGView.init(name: "icon_xf_pop_close")
button.scViewProperties("关闭按钮")
return button
}()
/// 显示的图片
lazy var iconView: UIImageView = {
let iconView = UIImageView()
iconView.contentMode = .scaleAspectFit
//打开点击交互
iconView.isUserInteractionEnabled = true
iconView.scViewProperties("显示的图片")
return iconView
}()
init(model: YPBuoyListModel) {
self.model = model
//根据关闭按钮的大小确定的偏移
let offest: CGFloat = 18.scale
let y: CGFloat = 300.scale
/// 设置浮标位置大小
if let size = model.size?.components(separatedBy: "*"),size.count==2{
let w = size[0].axc_cgFloat
let h = size[1].axc_cgFloat
let x = YPScreen.screen_w - w - offest
self.imgW = w
self.imgH = h
super.init(frame: CGRect(x, y, w + offest, h + offest))
}else{
self.imgW = 58
self.imgH = 58
let x = YPScreen.screen_w - self.imgW
super.init(frame: CGRect(x, y, self.imgW, self.imgH))
}
/// 设置图片
self.iconView.yp_load(model.iconUrl ?? "")
/// 后台配置了才加手势
if model.isDrag == 1{
//添加拖拽手势
let pan = UIPanGestureRecognizer(target: self, action: #selector(panAction(pan:)))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
self.addGestureRecognizer(pan)
}
/// 1 是需要关闭按钮
if model.isClose == 1 {
closeButton.isHidden = false
}else{
closeButton.isHidden = true
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// 初始化UI
override func makeUI() {
backgroundColor = .clear
addSubview(iconView)
iconView.snp.makeConstraints { (make) in
make.bottom.left.equalToSuperview()
make.width.equalTo(imgW)
make.height.equalTo(imgH)
}
/// 关闭按钮布局
addSubview(closeButton)
closeButton.snp.makeConstraints { (make) in
make.top.right.equalToSuperview()
make.size.equalTo(24.scale)
}
}
}
/// MARK- 拖拽事件
extension YPBuoyView {
@objc func panAction(pan: UIPanGestureRecognizer){
/// 不能拖拽直接返回
if dragEnable == false { return }
switch pan.state {
case .began:
/// 开始拖拽
pan.setTranslation(.zero, in: self)
startPoint = pan.translation(in: self)
case .changed: /// 拖拽改变
let point = pan.translation(in: self)
var dx: CGFloat = 0
var dy: CGFloat = 0
dx = point.x - startPoint.x
dy = point.y - startPoint.y
let newCenter = CGPoint(center.x + dx, center.y + dy)
center = newCenter
pan.setTranslation(.zero, in: self)
case .ended:
/// 拖拽结束
keepBounds()
default:
break
}
}
override func willMove(toSuperview newSuperview: UIView?) {
if (newSuperview != nil){
if freeRect == .zero{
freeRect = newSuperview?.bounds ?? .zero
}
keepBounds()
}
}
/// 停止拖拽后保持范围之内
func keepBounds() {
let centerX = CGFloat(freeRect.origin.x+(freeRect.size.width - frame.size.width)/2)
var rect = frame
/// 不吸边
if isKeepBounds == true {
if frame.origin.x < freeRect.origin.x {
UIView.animate(withDuration: 0.25) {[weak self] in
guard let weakSelf = self else {return}
rect.origin.x = weakSelf.freeRect.origin.x
weakSelf.frame = rect
}
}else if freeRect.origin.x+freeRect.size.width < frame.origin.x+frame.size.width {
UIView.animate(withDuration: 0.25) {[weak self] in
guard let weakSelf = self else {return}
rect.origin.x = weakSelf.freeRect.origin.x+weakSelf.freeRect.size.width-weakSelf.frame.size.width
weakSelf.frame = rect
}
}
}else{
if frame.origin.x < centerX {/// 小于中心点
UIView.animate(withDuration: 0.25) {[weak self] in
guard let weakSelf = self else {return}
rect.origin.x = weakSelf.freeRect.origin.x
weakSelf.frame = rect
}
}else{
UIView.animate(withDuration: 0.25) {[weak self] in
guard let weakSelf = self else {return}
rect.origin.x = weakSelf.freeRect.origin.x+weakSelf.freeRect.size.width - weakSelf.frame.size.width
weakSelf.frame = rect
}
}
}
let freeRectY = freeRect.origin.y+StatusHeight
let originFrame = frame.origin.y+frame.size.height
let freeRectH = freeRect.origin.y+freeRect.size.height
let originTotelFrame = originFrame+TabBarHeight
if frame.origin.y < freeRectY+130 { /// 修改范围
UIView.animate(withDuration: 0.25) {[weak self] in
guard let weakSelf = self else {return}
rect.origin.y = weakSelf.freeRect.origin.y+StatusHeight+130
weakSelf.frame = rect
}
}else if freeRectH < originTotelFrame{ /// 拖动到底部
UIView.animate(withDuration: 0.25) {[weak self] in
guard let weakSelf = self else {return}
rect.origin.y = weakSelf.freeRect.origin.y+weakSelf.freeRect.size.height-weakSelf.frame.size.height-TabBarHeight
weakSelf.frame = rect
}
}
/// 滚动了以后更新单例里面对应id的frame
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
if let currentVC = UIViewController.getCurrent {
YPBuoyViewManager.manager.addBuoyRect(vc: currentVC,
id: self.model.id ?? 0, rect: self.frame)
}
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
2016-04-07 swift集成alamofire的简单封装