SwiftUI应用开屏广告界面项目(三)

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
其余文件没有变化。

需求/思路分析

也就是说我们要在(二)的基础上添加时间戳判断和图片保存功能。

活动时间的判断比较简单,在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

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【课程特点】1、231节大容量课程:包含了SwiftUI的大部分知识点,详细讲解SwiftUI的方方面面;2、15个超级精彩的实例:包含美食、理财、健身、教育、电子商务等各行业的App实例;3、创新的教学模式:手把手教您SwiftUI用户界面开发技术,一看就懂,一学就会;4、贴心的操作提示:让您的眼睛始终处于操作的焦点位置,不用再满屏找光标;5、语言简洁精练:瞄准问题的核心所在,减少对思维的干扰,并节省您宝贵的时间;6、视频短小精悍:即方便于您的学习和记忆,也方便日后对功能的检索;7、齐全的学习资料:提供所有课程的源码,在Xcode 11 + iOS 13环境下测试通过; 更好的应用,更少的代码!SwiftUI是苹果主推的下一代用户界面搭建技术,具有声明式语法、实时生成界面预览等特性,可以为苹果手机、苹果平板、苹果电脑、苹果电视、苹果手表五个平台搭建统一的用户界面SwiftUI是一种创新、简单的iOS开发中的界面布局方案,可以通过Swift语言的强大功能,在所有的Apple平台上快速构建用户界面。 仅使用一组工具和API为任何Apple设备构建用户界面SwiftUI具有易于阅读和自然编写的声明式Swift语法,可与新的Xcode设计工具无缝协作,使您的代码和设计**同步。自动支持动态类型、暗黑模式、本地化和可访问性,意味着您的**行SwiftUI代码已经是您编写过的非常强大的UI代码了。 

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值