Swift UIKit程序化实现带底部箭头的自定义广告视图
基于UIKit实现带底部箭头的圆角广告视图(类似Waze广告框)
针对你的需求,我们可以在提供的UIKit代码基础上,通过自定义形状背景、添加图片视图、调整布局约束这几个核心步骤来实现目标效果,以下是完整的改造方案:
1. 先修正初始化的编译问题
原代码中advertismentModel作为存储属性未在初始化方法中赋值,会导致编译错误,我们先调整初始化逻辑:
import UIKit import Kingfisher // 补充代理协议(如果之前未定义) protocol AdvertismentAnnotationViewDelegate: AnyObject { func advertismentAnnotationViewDidClose(_ view: AdvertismentAnnotationView) func advertismentAnnotationView(_ view: AdvertismentAnnotationView, didSelect isSelected: Bool) } // 假设你的数据模型结构 struct AdvertismentModel { let imageURL: String // 可添加其他属性如标题、描述等 } final class AdvertismentAnnotationView: UIView { weak var delegate: AdvertismentAnnotationViewDelegate? let advertismentModel: AdvertismentModel // 改为let,初始化后不再修改 var selected: Bool = false { didSet { selectButton.setTitle(selected ? "Deselect" : "Select", for: .normal) } } // --- 替换原adImage为UIImageView,用于显示广告图 --- lazy var adImageView: UIImageView = { let imageView = UIImageView(frame: .zero) imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true imageView.layer.cornerRadius = 8 // 图片添加圆角 return imageView }() lazy var closeButton: UIButton = { let button = UIButton(type: .system) button.setTitleColor(.black, for: .normal) button.setTitle("X", for: .normal) button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16) return button }() lazy var selectButton: UIButton = { let button = UIButton(type: .system) button.setTitleColor(.white, for: .normal) button.backgroundColor = UIColor(red: 0, green: 0.478, blue: 0.988, alpha: 1) button.layer.cornerRadius = 8 button.clipsToBounds = true button.setTitle("Select", for: .normal) return button }() // 自定义初始化方法 init(frame: CGRect, model: AdvertismentModel) { self.advertismentModel = model super.init(frame: frame) setupView() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
2. 实现带底部箭头的白色圆角背景
通过CAShapeLayer绘制自定义路径,实现带箭头的圆角矩形背景,同时添加阴影增强立体感:
extension AdvertismentAnnotationView { private func setupArrowBackground() { let shapeLayer = CAShapeLayer() shapeLayer.fillColor = UIColor.white.cgColor // 添加阴影 shapeLayer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor shapeLayer.shadowOffset = CGSize(width: 0, height: 2) shapeLayer.shadowOpacity = 0.5 shapeLayer.shadowRadius = 4 // 定义形状参数 let cornerRadius: CGFloat = 12 let arrowWidth: CGFloat = 20 let arrowHeight: CGFloat = 10 // 绘制路径:圆角矩形 + 底部中间箭头 let path = UIBezierPath() path.move(to: CGPoint(x: cornerRadius, y: 0)) // 右上角圆角 path.addArc(withCenter: CGPoint(x: bounds.width - cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: .pi * 3/2, endAngle: 0, clockwise: true) // 右下角圆角 path.addArc(withCenter: CGPoint(x: bounds.width - cornerRadius, y: bounds.height - cornerRadius - arrowHeight), radius: cornerRadius, startAngle: 0, endAngle: .pi/2, clockwise: true) // 箭头右端点 path.addLine(to: CGPoint(x: bounds.width/2 + arrowWidth/2, y: bounds.height - cornerRadius - arrowHeight)) // 箭头底端点 path.addLine(to: CGPoint(x: bounds.width/2, y: bounds.height)) // 箭头左端点 path.addLine(to: CGPoint(x: bounds.width/2 - arrowWidth/2, y: bounds.height - cornerRadius - arrowHeight)) // 左下角圆角 path.addArc(withCenter: CGPoint(x: cornerRadius, y: bounds.height - cornerRadius - arrowHeight), radius: cornerRadius, startAngle: .pi/2, endAngle: .pi, clockwise: true) // 左上角圆角 path.addArc(withCenter: CGPoint(x: cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: .pi, endAngle: .pi * 3/2, clockwise: true) shapeLayer.path = path.cgPath layer.insertSublayer(shapeLayer, at: 0) } // 重写layoutSubviews,确保视图尺寸变化时重新绘制背景 override func layoutSubviews() { super.layoutSubviews() // 移除旧的形状层 layer.sublayers?.filter { $0 is CAShapeLayer }.forEach { $0.removeFromSuperlayer() } setupArrowBackground() } }
3. 配置视图布局与图片加载
将图片视图、按钮添加到视图层级,设置约束并通过Kingfisher加载网络图片:
extension AdvertismentAnnotationView { private func setupView() { backgroundColor = .clear // 背景设为透明,用形状层做实际背景 // 添加按钮点击事件 closeButton.addTarget(self, action: #selector(closePressed(sender:)), for: .touchUpInside) selectButton.addTarget(self, action: #selector(selectPressed(sender:)), for: .touchUpInside) // 添加子视图到层级 [adImageView, closeButton, selectButton].forEach { $0.translatesAutoresizingMaskIntoConstraints = false addSubview($0) } setupArrowBackground() setupConstraints() loadAdImage() } private func setupConstraints() { NSLayoutConstraint.activate([ // 关闭按钮:右上角 closeButton.topAnchor.constraint(equalTo: topAnchor, constant: 8), closeButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), closeButton.widthAnchor.constraint(equalToConstant: 24), closeButton.heightAnchor.constraint(equalToConstant: 24), // 广告图片:位于关闭按钮下方,左右留边,固定高度 adImageView.topAnchor.constraint(equalTo: closeButton.bottomAnchor, constant: 8), adImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), adImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), adImageView.heightAnchor.constraint(equalToConstant: 120), // 选择按钮:图片下方,避开箭头区域 selectButton.topAnchor.constraint(equalTo: adImageView.bottomAnchor, constant: 8), selectButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), selectButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), selectButton.heightAnchor.constraint(equalToConstant: 32), selectButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20) ]) } private func loadAdImage() { guard let imageURL = URL(string: advertismentModel.imageURL) else { return } // Kingfisher加载网络图片 adImageView.kf.setImage(with: imageURL) } // 按钮事件实现 @objc private func closePressed(sender: UIButton) { delegate?.advertismentAnnotationViewDidClose(self) } @objc private func selectPressed(sender: UIButton) { selected.toggle() delegate?.advertismentAnnotationView(self, didSelect: selected) } }
4. 在MapBox中使用该视图
在你的地图代理方法中注册并返回该AnnotationView:
// 假设你有自定义的Annotation类型 class AdvertismentAnnotation: MGLPointAnnotation { let model: AdvertismentModel init(coordinate: CLLocationCoordinate2D, model: AdvertismentModel) { self.model = model super.init() self.coordinate = coordinate } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // 在MapViewDelegate中 func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { guard let adAnnotation = annotation as? AdvertismentAnnotation else { return nil } let reuseID = "AdAnnotation" // 复用或创建新视图 if let view = mapView.dequeueReusableAnnotationView(withIdentifier: reuseID) as? AdvertismentAnnotationView { return view } else { return AdvertismentAnnotationView(frame: CGRect(x: 0, y: 0, width: 200, height: 200), model: adAnnotation.model) } }
内容的提问来源于stack exchange,提问作者RebinX1
相关产品推荐
相关产品推荐

