You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 09:50:34