SwiftUI应用开屏广告界面项目(三)
需求
在(二)的基础上,添加以下需求:
拉取到远端数据之后,如果发现此次开屏活动还没有过期,则将图片下载到本地,并进行相关配置,使得下次开屏可以正常显示开屏活动图片。
源码
ContentView.swift
//
// ContentView.swift
// core1
//
// Created by WMIII on 2021/4/3.
//
import SwiftUI
import UIKit
import Combine
import CoreData
class TimeHelp {
var canceller: AnyCancellable?
//每次都新建一个计时器
func start(receiveValue: @escaping (() -> Void)) {
let timerPublisher = Timer
.publish(every: 1, on: .main, in: .common)
.autoconnect()
self.canceller = timerPublisher.sink { date in
receiveValue()
}
}
//暂停销毁计时器
func stop() {
canceller?.cancel()
canceller = nil
}
}
struct Advertisement: Codable {
// var id = UUID()
var picUrl: String
var showTime: Int
var timestamp: Int64
}
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Ads.picUrl, ascending: true)],
animation: .default)
private var ads: FetchedResults<Ads>
@State private var remoteImage :UIImage? = nil
@State var isPresented = false
let placeholderOne = UIImage(named: "Image1")
let timer = Timer.publish(every: 1, on: .main, in: .common)
@State private var second = 3
private let timeHelper = TimeHelp()
@State private var end = true
@State var adsJson: [Advertisement] = [] // 这里去掉@State会发生很奇怪的事
var sand = SandBox()
var body: some View {
ZStack
{
Button("跳过 \(second)"){
self.isPresented = true
}
.position(x: UIScreen.main.bounds.width - 45, y: 10.0)
.onAppear()
{
for ad in ads {
print(ad.picUrl!)
print(ad.showTime)
}
guard self.end else {return}
self.end = false
self.second = 3
self.timeHelper.start {
if self.second > 1 {
_ = self.second -= 1
}else{
// 暂停
self.end = true
self.timeHelper.stop()
self.isPresented = true
}
}
}
.fullScreenCover(isPresented: $isPresented) {
print("消失")
} content: {
DetailView(message: "I'm missing you")
}
Image(uiImage: self.remoteImage ?? placeholderOne!)
// Image(uiImage: self.placeholderOne!)
.resizable()
.scaledToFit()
// .aspectRatio(contentMode: .fill)
.onAppear(perform: fetchRemoteImg)
}
}
func fetchRemoteImg()
{
getAdJson()
if ads.count != 0
{
let timeStamp = Int(NSDate().timeIntervalSince1970)
for index in 0...(ads.count - 1)
{
if ads[index].timestamp < timeStamp
{
deleteItems(offsets: [index])
print("删除了一个已过期活动")
break
}
}
if ads.count == 0
{
return
}
let showad = ads[ads.count - 1]
let fullPath = NSHomeDirectory().appending("/Documents/").appending(showad.picUrl!)
if let savedImg = UIImage(contentsOfFile: fullPath)
{
remoteImage = savedImg
ads[ads.count - 1].showTime -= 1
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
if ads[ads.count - 1].showTime == 0 {
deleteItems(offsets: [ads.count - 1])
}
}
else
{
print("文件不存在")
}
}
else
{
return
}
}
func getAdJson()
{
// 测试用URL地址
let urlAddress = "http://127.0.0.1:8000/api/getjson"
guard let adurl = URL(string: urlAddress) else {return}
URLSession.shared.dataTask(with: adurl) {
(data, response, error) in
do {
if let d = data
{
let jItem = try JSONDecoder().decode(Advertisement.self, from: d)
DispatchQueue.main.async {
addAd(adjson: jItem)
}
}
else
{
print("no data.")
}
}
catch
{
print("error")
}
}.resume()
}
func isAdExist(adname: String) -> Bool {
for ad in ads {
if adname == ad.picUrl
{
return true
}
}
return false
}
private func addAd(adjson: Advertisement) {
let arraySubStrings: [Substring] = adjson.picUrl.split(separator: "/")
let arrayStrings: [String] = arraySubStrings.compactMap { "\($0)" }
let length = arrayStrings.count
if isAdExist(adname: arrayStrings[length - 1])
{
return
}
withAnimation {
guard let url = URL(string: adjson.picUrl) else {return}
URLSession.shared.dataTask(with: url)
{
(data, response, error) in
if let img = UIImage(data: data!)
{
let newAd = Ads(context: viewContext)
newAd.picUrl = arrayStrings[length - 1]
newAd.showTime = Int32(adjson.showTime)
newAd.timestamp = adjson.timestamp
do {
sand.saveImage(currentImage: img, persent: 100, imageName: newAd.picUrl!)
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
else
{
print(error ?? "1")
}
}
.resume()
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { ads[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct DetailView: View{
let message: String
var body: some View {
VStack
{
Text(message)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
SandBox.swift
SandBox.swift为我自己新建的文件,里面是一些对沙盒文件的操作。
//
// SandBox.swift
// core1
//
// Created by WMIII on 2021/4/4.
//
import Foundation
import SwiftUI
import UIKit
import Combine
import CoreData
struct SandBox {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Ads.picUrl, ascending: true)],
animation: .default)
private var ads: FetchedResults<Ads>
//保存图片至沙盒
func saveImage(currentImage: UIImage, persent: CGFloat, imageName: String){
if let imageData = currentImage.jpegData(compressionQuality: persent) as NSData? {
let fullPath = NSHomeDirectory().appending("/Documents/").appending(imageName)
imageData.write(toFile: fullPath, atomically: true)
print("fullPath=\(fullPath)")
}
}
// 函数我放这了,但是我并没有使用它
func removefile(folderName: String){
if folderName == ""{
return
}
let fileManager = FileManager.default
try! fileManager.removeItem(atPath: folderName)
}
}
CoreData
其余文件没有变化。
需求/思路分析
也就是说我们要在(二)的基础上添加时间戳判断和图片保存功能。
活动时间的判断比较简单,在CoreData中的Ads实体内和Advertisement类中再加一个名为timestamp的Int属性的字段,用于保存活动截止时间的时间戳,同时后端也需要传递活动截止时间戳;在每次开屏显示之前遍历一遍CoreData中活动的截止时间戳,若有过期的则直接删除,并停止遍历。
不过图片文件的保存稍微复杂一点点。最初我打算使用CoreData保存UIImage对象,但经过实验之后发现貌似并不能这么干;于是我还是老老实实用沙盒保存图像文件。
为了满足要求,我对如下几个函数/方法进行了重点添加/改进:
func saveImage()
//保存图片至沙盒
func saveImage(currentImage: UIImage, persent: CGFloat, imageName: String){
if let imageData = currentImage.jpegData(compressionQuality: persent) as NSData? {
let fullPath = NSHomeDirectory().appending("/Documents/").appending(imageName)
imageData.write(toFile: fullPath, atomically: true)
print("fullPath=\(fullPath)")
}
}
该函数在SandBox.swift中;
该函数将指定了文件名的UIImage以图片形式保存在沙盒的Document目录下;
func isExist()
func isAdExist(adname: String) -> Bool {
for ad in ads {
if adname == ad.picUrl
{
return true
}
}
return false
}
该函数判断传入的文件名在沙盒中是否存在。
func addAd()
该函数就是之前的addItem(),我只是将它换了个名字。
private func addAd(adjson: Advertisement) {
let arraySubStrings: [Substring] = adjson.picUrl.split(separator: "/")
let arrayStrings: [String] = arraySubStrings.compactMap { "\($0)" }
let length = arrayStrings.count
if isAdExist(adname: arrayStrings[length - 1])
{
return
}
withAnimation {
guard let url = URL(string: adjson.picUrl) else {return}
URLSession.shared.dataTask(with: url)
{
(data, response, error) in
if let img = UIImage(data: data!)
{
let newAd = Ads(context: viewContext)
newAd.picUrl = arrayStrings[length - 1]
newAd.showTime = Int32(adjson.showTime)
newAd.timestamp = adjson.timestamp
do {
sand.saveImage(currentImage: img, persent: 100, imageName: newAd.picUrl!)
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
else
{
print(error ?? "1")
}
}
.resume()
}
}
在这个函数中,我并没有直接将远端传送过来的图像URL地址直接保存在Advertisement.picUrl中,而是将这个地址的UIImage调用saveImage()保存在了本地沙盒,文件名为URL的最后一段路径,然后将本地的文件路径保存在Advertisement.picUrl中,并保存在CoreData内。
同时,这个函数调用了isExist()函数,判断本地是否保存有同名图片文件(活动);如果有则放弃保存,解决了重复数据的问题。
注意,将本地图片转换成UIImage时是不会管图像文件名是否有后缀、图像格式是什么的,所以只要后端能保证相同图片下发的json数据是一样的就行。
func fetchRemoteImg()
实际上这个函数已经不再从远端获取图片了…
func fetchRemoteImg()
{
getAdJson()
if ads.count != 0
{
let timeStamp = Int(NSDate().timeIntervalSince1970)
for index in 0...(ads.count - 1)
{
if ads[index].timestamp < timeStamp
{
deleteItems(offsets: [index])
print("删除了一个已过期活动")
break
}
}
if ads.count == 0
{
return
}
let showad = ads[ads.count - 1]
let fullPath = NSHomeDirectory().appending("/Documents/").appending(showad.picUrl!)
if let savedImg = UIImage(contentsOfFile: fullPath)
{
remoteImage = savedImg
ads[ads.count - 1].showTime -= 1
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
if ads[ads.count - 1].showTime == 0 {
deleteItems(offsets: [ads.count - 1])
}
}
else
{
print("文件不存在")
}
}
else
{
return
}
}
该函数首先先尝试从远端拉取数据,然后遍历判断并删除过期的活动;最后通过CoreData显示本地最新的活动。
不足
在遍历CoreData时,每次遍历只要找到一个过期活动并删除后,就会跳出遍历;若存在多个活动过期则只会删除最旧的活动。不过我觉得问题不大,毕竟正常应用也不会几十个活动同时过期。
同时在活动被删除时,我并没有删除沙盒中的图片文件。
有点始乱终弃,极其不负责任…
象征性结果
代码和运行效果录屏我放某度网盘里面了。
链接: https://pan.baidu.com/s/1Kde3kip9AKTtIHvN1vX9mQ
密码: u004