iOS自定义控件:开屏广告
2019-09-25 本文已影响0人
今晚月色
专用整理图
很久没有去写博客了,最近由于公司需求变化,需要做一个新的开屏广告的页面,所以我就简单的去研究了一下。其中显示分为视频和图片,其中图片相比而言实现起来就简单的多了。
图片加载
可以使用第三方的加载和缓存,例如:SDWebImage
或者Kingfisher
。当然也可以使用系统的URLSession
下载到本地再进行读取加载。
视频加载
我这边利用的就URLSession
的downloadTask
进行下载到本地再进行播放的。
视频下载的代码
import UIKit
/// 保存URL地址进行匹配
fileprivate let kURL_DOWNLOAD_PATH = "WDDownloadUrlPath"
/// 保存路径
fileprivate let kDOCUMENTS_PATH = NSHomeDirectory() + "/Documents/Advertisement.mp4"
fileprivate class UserDefaultsTools {
// 设置地址
static func wd_set(value: String) {
UserDefaults.standard.setValue(value, forKey: kURL_DOWNLOAD_PATH)
}
// 读取地址
static func wd_get() -> String {
return UserDefaults.standard.object(forKey: kURL_DOWNLOAD_PATH) as? String ?? ""
}
}
fileprivate class FileManagerTools {
// 是否存在文件
static func isExistFile(atPath: String) -> Bool {
return FileManager.default.fileExists(atPath: atPath)
}
// 移动文件
static func moveFile(fromPath: String, toPath: String) {
try! FileManager.default.moveItem(atPath: fromPath, toPath: toPath)
}
// 删除文件
static func deleteFile(atPath: String) {
try! FileManager.default.removeItem(atPath: atPath)
}
// 文件大小
static func fileSize(atPath: String) -> Float {
if self.isExistFile(atPath: kDOCUMENTS_PATH) {
let attributes = try! FileManager.default.attributesOfItem(atPath: atPath)
let size = attributes[FileAttributeKey.size] as! Int
return Float(size)
}
return 0
}
}
class WDLaunchADDownloader: NSObject {
/// 下载视频的路径
private var downloadUrlPath: String = ""
/// 下载的Session
private var session: URLSession!
/// 下载的任务
private var downloadTask: URLSessionDownloadTask!
/// 记录是否在下载
private var downloading: Bool = false
/// 记录已下载的数据
private var downloadData: Data?
/* 下载视频 */
static func downloadVideo(path: String) -> String {
let downloader = WDLaunchADDownloader()
return downloader.setupDownload(with: path)
}
private func setupDownload(with path: String) -> String {
downloadUrlPath = path // 获取视频播放网络路径
let configuration = URLSessionConfiguration.default
configuration.isDiscretionary = true
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let request = URLRequest.init(url: URL(string: path)!)
downloadTask = session.downloadTask(with: request)
// 网址不同,删除原本的,下载最新的
if UserDefaultsTools.wd_get() != path {
UserDefaultsTools.wd_set(value: "")
if FileManagerTools.isExistFile(atPath: kDOCUMENTS_PATH) {
FileManagerTools.deleteFile(atPath: kDOCUMENTS_PATH)
}
// 开始下载
startDownload()
}
// 如果已存在文件 就加载视频 否则加载图片
if FileManagerTools.isExistFile(atPath: kDOCUMENTS_PATH) {
return kDOCUMENTS_PATH
} else {
return ""
}
}
// MARK: - 开始下载
private func startDownload() {
downloading = true
downloadTask.resume()
}
// MARK: - 暂停下载
private func pauseDownload() {
downloadTask.cancel { [weak self] (resumeData) in
self?.downloadData = resumeData
}
downloading = false
}
}
extension WDLaunchADDownloader: URLSessionDownloadDelegate {
// 下载代理方法,下载结束
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let locationPath = location.path
FileManagerTools.moveFile(fromPath: locationPath, toPath: kDOCUMENTS_PATH)
UserDefaultsTools.wd_set(value: downloadUrlPath)
}
// 下载代理方法,监听下载进度
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if FileManagerTools.fileSize(atPath: kDOCUMENTS_PATH) >= Float(totalBytesExpectedToWrite) {
pauseDownload()
return
}
print(String(format: "🍉 Advertising Video Download Progress: %0.2f", Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)))
}
// 如果下载失败了
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
UserDefaultsTools.wd_set(value: "")
}
}
}
视频播放代码
其中获取到加载的种类(图片、视频),页面进行相应的布局
import UIKit
import AVFoundation
class WDLaunchADPlayerManager: NSObject {
static let share = WDLaunchADPlayerManager()
var playerItem: AVPlayerItem!
var player: AVPlayer!
var playerLayer: CALayer!
// 开始播放 <播放下载好再本地文件夹里面的视频文件>
func playItem(with url: String) {
playerItem = AVPlayerItem(url: URL(fileURLWithPath: url))
player = AVPlayer(playerItem: playerItem)
playerLayer = AVPlayerLayer(player: player)
player.usesExternalPlaybackWhileExternalScreenIsActive = true
player.play()
player.volume = 0 // 静音播放
}
// 停止播放
func stopPlay() {
player.pause()
player = nil
}
}
页面显示
import UIKit
import SDWebImage
class WDLaunchADController: UIViewController {
/// 播放器的layer层
private var playerLayer: CALayer = CALayer()
/// 图片加载
private let imageView: UIImageView = {
let imageV = UIImageView()
imageV.contentMode = .scaleAspectFill
imageV.clipsToBounds = true
return imageV
}()
deinit {
print("==[\(type(of: self))] deinit==")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
}
// 配置页面样式
func setup(with adType: WDLaunchADType, url: String, bottomLogoView: UIView? = UIView(), bottomLogoViewHeight: CGFloat = 180) {
if adType == .image {
imageView.isUserInteractionEnabled = true
view.addSubview(imageView)
imageView.sd_setImage(with: URL(string: url), completed: nil)
} else {
WDLaunchADPlayerManager.share.playItem(with: url)
if let layer = WDLaunchADPlayerManager.share.playerLayer {
self.view.layer.addSublayer(layer)
playerLayer = layer
}
}
if let logoView = bottomLogoView {
view.addSubview(logoView)
logoView.frame = CGRect(x: 0, y: view.frame.height - bottomLogoViewHeight, width: view.frame.width, height: bottomLogoViewHeight)
if adType == .image {
imageView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height - bottomLogoViewHeight)
} else {
playerLayer.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
}
} else {
if adType == .image {
imageView.frame = view.bounds
} else {
playerLayer.frame = view.bounds
}
}
}
override var shouldAutorotate: Bool {
return false
}
override var prefersHomeIndicatorAutoHidden: Bool {
return false
}
}
加载控制代码
import UIKit
enum WDLaunchADType {
case image
case video
}
/// 是否是刘海屏系列
var kIS_IPHONEX: Bool {
var iPhoneX = false
if UIDevice.current.userInterfaceIdiom != .phone {
return iPhoneX
}
if #available(iOS 11.0, *) {
if let mainWindow = UIApplication.shared.delegate?.window {
if (mainWindow?.safeAreaInsets.bottom)! > CGFloat(0.0) {
iPhoneX = true
}
}
}
return iPhoneX
}
/// 状态栏高度
var kSTATUS_HEIGHT: CGFloat {
return kIS_IPHONEX ? UIApplication.shared.statusBarFrame.height : 22
}
// MARK: - Main
class WDLaunchAD: NSObject {
// MARK: Public
/// 静态方法
///
/// - Parameters:
/// - adType: 广告类型 `图片` `视频<无声音>`
/// - url: 广告地址
/// - durition: 显示时间
/// - bottomView: 底部视图
/// - buttomViewHeight: 底部视图高度
/// - clickBlock: 广告点击事件回调
static func setup(with adType: WDLaunchADType = .image,
url: String, durition: Int,
bottomView: UIView? = UIView(),
buttomViewHeight: CGFloat = 0,
clickBlock: @escaping (() -> Void)) {
let manager = WDLaunchAD()
manager.show(with: adType, url: url, durition: durition, bottomView: bottomView, buttomViewHeight: buttomViewHeight ,clickBlock: clickBlock)
}
// MARK: Private
/// 页面显示的window根控制器,除了倒计时按钮,其他元素在控制器中配置
private let wdLaunchADViewController = WDLaunchADController()
/// 显示的时间
private var timerInterval: Int = 0
/// 定时器
private var timer: Timer?
/// 点击事件回调
private var clickBlockHandle: (() -> Void)?
/// 页面显示的window
private let window: UIWindow = {
let window = UIWindow.init(frame: UIScreen.main.bounds)
window.rootViewController?.view.backgroundColor = .black
window.rootViewController?.view.isUserInteractionEnabled = false
window.windowLevel = UIWindow.Level.statusBar + 1
window.isHidden = false
window.alpha = 1
return window
}()
/// 倒计时按钮
private let timeButton: UIButton = {
let button = UIButton(type: .custom)
button.backgroundColor = UIColor(white: 0, alpha: 0.5)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 15)
return button
}()
}
// MARK: - 页面初始化与销毁
fileprivate extension WDLaunchAD {
// 控制器初始化页面
func show(with adType: WDLaunchADType, url: String, durition: Int, bottomView: UIView?, buttomViewHeight: CGFloat, clickBlock: @escaping (() -> Void)) {
clickBlockHandle = clickBlock
timerInterval = durition
window.rootViewController = wdLaunchADViewController
wdLaunchADViewController.view.addGestureRecognizer(UITapGestureRecognizer.init(target: self, action: #selector(adGuestureTargetAction)))
wdLaunchADViewController.setup(with: adType, url: url, bottomLogoView: bottomView, bottomLogoViewHeight: buttomViewHeight)
addTimeButton()
// 开始倒计时
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.timerDown(timer:)), userInfo: nil, repeats: true)
if let timer = self.timer {
timer.fire()
}
}
}
// 移除定时器与视图
func removeWindow() {
// 停止播放视频
if WDLaunchADPlayerManager.share.player != nil {
WDLaunchADPlayerManager.share.stopPlay()
}
// 停止倒计时并移除
if let timer = timer {
timer.invalidate()
}
timer = nil
// 移除当前视图
UIView.transition(with: window, duration: 0.8, options: UIView.AnimationOptions.curveEaseOut, animations: {
self.window.alpha = 0;
}) { (finished) in
self.window.rootViewController = nil
self.window.removeFromSuperview()
}
}
}
// MARK: - 页面控件配置
private extension WDLaunchAD {
// 配置时间按钮
func addTimeButton() {
timeButton.setTitle(" 跳过 \(timerInterval) ", for: .normal)
timeButton.addTarget(self, action: #selector(buttonTargetAction(sender:)), for: .touchUpInside)
window.addSubview(timeButton)
timeButton.translatesAutoresizingMaskIntoConstraints = false
window.addConstraints([
NSLayoutConstraint(item: timeButton, attribute: .trailing, relatedBy: .equal, toItem: window, attribute: .trailing, multiplier: 1, constant: -15),
NSLayoutConstraint(item: timeButton, attribute: .top, relatedBy: .equal, toItem: window, attribute: .top, multiplier: 1, constant: kSTATUS_HEIGHT),
])
timeButton.layoutIfNeeded()
timeButton.layer.cornerRadius = timeButton.frame.height / 2.0
}
}
// MARK: - 倒计时与倒计时点击事件
@objc private extension WDLaunchAD {
// 倒计时
func timerDown(timer: Timer) {
timerInterval -= 1
timeButton.setTitle(" 跳过 \(timerInterval) ", for: .normal)
if timerInterval == 0 {
removeWindow()
}
}
// 广告页移除
func buttonTargetAction(sender: UIButton) {
removeWindow()
}
// 点击手势
func adGuestureTargetAction() {
if clickBlockHandle != nil {
clickBlockHandle!()
}
}
}
加载方式
在AppDelgate
中的didFinishLaunchingWithOptions
方法中调用方法。
我这为了适应变化的需要,提供了相对自由的显示方式
底部的logo视图是可以自定义的,传入对应显示的高度即可。
图片地址
let imagePath = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1567682065374&di=4c2a4d2008a4e951b34b3d130db19d12&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201704%2F14%2F20170414155109_Cmz24.jpeg"
- 仅显示图片
WDLaunchAD.setup(with: .image, url: imagePath, durition: 5, bottomView: WDLaunchADBottomLogoView(), buttomViewHeight: 180) {
print("✨ 点击了广告")
}
仅显示图片
- 显示图片与底部logo视图
底部视图代码
// MARK: - 底部logo视图
class WDLaunchADBottomLogoView: UIView {
// logo
let logoImageView: UIImageView = {
let imageV = UIImageView()
imageV.image = UIImage(named: "nav-logo-4c7bbafe27adc892f3046e6978459bac")?.withRenderingMode(.alwaysTemplate)
imageV.tintColor = .red
return imageV
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
setupSubviewsLayouts()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: 底部logo视图页面配置与布局
extension WDLaunchADBottomLogoView {
func setupSubviews() {
backgroundColor = .white
addSubview(logoImageView)
logoImageView.translatesAutoresizingMaskIntoConstraints = false
}
func setupSubviewsLayouts() {
addConstraints([
NSLayoutConstraint(item: logoImageView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: logoImageView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0)
])
}
}
加载代码
WDLaunchAD.setup(with: .image, url: imagePath, durition: 5, bottomView: WDLaunchADBottomLogoView(), buttomViewHeight: 180) {
print("✨ 点击了广告")
}
图片+底部logo视图
- 加载视频
首先需要现在有网的环境缓存到本地,我这边的思想是,首次进入App默认加载图片,然后下载广告的视频到本地。再次进入的时候,判断是否是缓存的视频,是则进行播放。是否加载的URL
与缓存的不同,则依旧加载图片,并下载最新URL
下的视频。
let path = WDLaunchADDownloader.downloadVideo(path: "http://wvideo.spriteapp.cn/video/2018/0514/eede6198571f11e8b5ca842b2b4c75ab_wpd.mp4")
if path.count > 0 {
WDLaunchAD.setup(with: .video, url: path, durition: 5, bottomView: WDLaunchADBottomLogoView(), buttomViewHeight: 180) {
print("✨ 点击了广告")
}
} else {
WDLaunchAD.setup(with: .image, url: imagePath, durition: 5, bottomView: WDLaunchADBottomLogoView(), buttomViewHeight: 180) {
print("✨ 点击了广告")
}
}
视频播放效果图,没有找到合适大小的视频就凑合着用了。
说明:
- 页面消失的动画,没有做过多的处理
- 视频没有做循环播放