DownloadingImages 下载缓存图片,显示图片文字列表

news/2024/7/20 23:09:34 标签: iOS, Swift, UI

1. 用到的技术点:

  1) Codable : 可编/解码 JSON 数据

  2) background threads : 后台线程

  3) weak self : 弱引用

  4) Combine : 取消器/组合操作

  5) Publishers and Subscribers : 发布者与订阅者

  6) FileManager : 文件管理器

  7) NSCache : 缓存

2. 网址:

  2.1 测试接口网址:

jsonplaceholdericon-default.png?t=N7T8https://jsonplaceholder.typicode.com/

  2.2 JSON 转 Model 网址:

quicktypeicon-default.png?t=N7T8https://app.quicktype.io/

3. 项目结构图

4. Model 层

  4.1 创建 PhotoModel.swift 文件

Swift">import Foundation

struct PhotoModel: Identifiable, Codable{
    let albumId: Int
    let id: Int
    let title: String
    let url: String
    let thumbnailUrl: String
}

/*
 {
   "albumId": 1,
   "id": 1,
   "title": "accusamus beatae ad facilis cum similique qui sunt",
   "url": "https://via.placeholder.com/600/92c952",
   "thumbnailUrl": "https://via.placeholder.com/150/92c952"
 }
 */

5. 工具类

  5.1 创建请求数据服务类,PhotoModelDataService.swift

Swift">import Foundation
import Combine

/// 请求数据服务
class PhotoModelDataService{
    
    // 单例模式 Singleton
    static let instance = PhotoModelDataService()
    // 返回 JSON 数据,解码成模型
    @Published var photoModel:[PhotoModel] = []
    // 随时取消请求
    var cancellables = Set<AnyCancellable>()
    
    // 只能内部实例化,保证一个 App 只有一次实例化
    private init() {
        downloadData()
    }
    
    // 测试接口网址: https://jsonplaceholder.typicode.com/
    // 下载数据
    func downloadData(){
        // 获取 URL
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/photos") else { return }
        
        // 进行请求
        URLSession.shared.dataTaskPublisher(for: url)
            .subscribe(on: DispatchQueue.global(qos: .background))
            .receive(on: DispatchQueue.main)
            .tryMap(handleOutput)
            .decode(type: [PhotoModel].self, decoder: JSONDecoder())
            .sink { completion in
                switch(completion){
                case .finished:
                    break
                case .failure(let error):
                    print("Error downloading data. \(error)")
                    break
                }
            } receiveValue: { [weak self] returnedPhotoModel in
                guard let self  = self else { return }
                self.photoModel = returnedPhotoModel
            }
            // 随时取消
            .store(in: &cancellables)
            
    }
    
    // 输出数据
    private func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data{
        guard
            let response = output.response as? HTTPURLResponse,
            response.statusCode >= 200 && response.statusCode < 300 else {
            throw URLError(.badServerResponse)
        }
        return output.data
    }
}

  5.2 创建图片缓存管理器类,PhotoModelCacheManager.swift

Swift">import Foundation
import SwiftUI

/// 图片缓存管理器
class PhotoModelCacheManager{
    // 单例模式
    static let instance = PhotoModelCacheManager()
    // 只能内部实例化,保证一个 App 只有一次实例化
    private init() {}
    // 图片数量缓存,计算型属性
    var photoCache: NSCache<NSString, UIImage> = {
        let cache = NSCache<NSString, UIImage>()
        cache.countLimit = 200
        cache.totalCostLimit = 1024 * 1024 * 200  // 200mb
        return cache
    }()
    
    // 添加
    func add(key: String, value: UIImage){
        photoCache.setObject(value, forKey: key as NSString)
    }
    
    // 获取
    func get(key: String) -> UIImage? {
        return photoCache.object(forKey: key as NSString)
    }
}

  5.3 创建储存图片文件管理类,PhotoModelFileManager.swift

Swift">import Foundation
import SwiftUI

// 存储图片文件管理器
class PhotoModelFileManager{
    // 单例模式
    static let instance = PhotoModelFileManager()
    let folderName = "downloaded_photos"
    
    private init(){
        createFolderIfNeeded()
    }
    
    // 创建存放图片的目录
    private func createFolderIfNeeded(){
        guard let url = getFolderPath() else { return }

        if !FileManager.default.fileExists(atPath: url.path){
            do {
                try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
                print("Created folder success.")
            } catch let error {
                print("Error creating folder. \(error)")
            }
        }
    }
    
    // 创建文件夹路径
    private func getFolderPath()-> URL?{
        return FileManager
            .default
            .urls(for: .cachesDirectory, in: .userDomainMask)
            .first?
            .appendingPathComponent(folderName)
    }
    
    // .../downloaded_photos
    // .../downloaded_photos/image_name.png
    /// 获取图片路径
    /// - Parameter key: 名字
    /// - Returns: 图片路径
    private func getImagePath(key: String) -> URL?{
        guard let folder = getFolderPath() else { return nil}
        return folder.appendingPathComponent(key + ".png")
    }
    
    // 添加图片
    func add(key: String, value: UIImage){
        // 获取数据和路径
        guard let data = value.pngData(),
              let url  = getImagePath(key: key) else { return }
        // 文件写人数据
        do {
            try data.write(to: url)
            print("Saving to file success.")
        } catch let error {
            print("Error saving to file manager. \(error)")
        }
    }
  
    // 获取图片
    func get(key: String) -> UIImage?{
        guard
            let path = getImagePath(key: key)?.path,
            FileManager.default.fileExists(atPath: path) else {
            //print("Error getting path.")
            return nil
        }
        return UIImage(contentsOfFile: path)
    }
}

6. ViewModel 层

  6.1 创建下载图片 ViewModel 类,DownloadingImageViewModel.swift

Swift">import Foundation
import Combine

class DownloadingImageViewModel: ObservableObject{
    
    // 数组模型
    @Published var dataArray:[PhotoModel] = []
    // 请求数据服务
    let dataService = PhotoModelDataService.instance
    // 取消操作
    var cancellables = Set<AnyCancellable>()
    
    init() {
        addSubscribers()
    }
    
    // 订阅数据
    func addSubscribers(){
        dataService.$photoModel
            .sink {[weak self] returnedPhotoModel in
                guard let self = self else { return }
                self.dataArray = returnedPhotoModel
            }
            .store(in: &cancellables)
    }
}

  6.2 创建图片加载 ViewModel 类,ImageLoadingViewModel.swift

Swift">import Foundation
import SwiftUI
import Combine

class ImageLoadingViewModel: ObservableObject{
    
    @Published var image: UIImage?
    @Published var isLoading: Bool = false
   
    // 取消
    var cancellables = Set<AnyCancellable>()
    
    // 缓存管理器
    let manager = PhotoModelFileManager.instance
    
    let urlString: String
    let imageKey: String
    
    init(url: String, key: String) {
        urlString = url
        imageKey = key
        getImage()
    }
    
    // 获取图片
    func getImage() {
        if let saveImage =  manager.get(key: imageKey){
            image = saveImage
            print("Getting saved image.")
        }else{
            downLoadImage()
            print("Downloading image now!")
        }
    }
    
    // 下载图片
    func downLoadImage(){
        isLoading = true
        guard let url = URL(string: urlString) else {
            isLoading = false
            return
        }
        
        // 请求
        URLSession.shared.dataTaskPublisher(for: url)
            .map { UIImage(data: $0.data) }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.isLoading = false
            } receiveValue: { [weak self] returnedImage in
                guard
                    let self = self,
                    let image = returnedImage else { return }
                self.image = image
                // 下载的图像保存在缓存中
                self.manager.add(key: imageKey, value: image)
            }
            .store(in: &cancellables)
    }
}

7. 创建 View 层

  7.1 创建下载,缓存,显示图片视图,DownloadingImageView.swift

Swift">import SwiftUI

/// 下载,缓存,显示图片
struct DownloadingImageView: View {
    @StateObject var loaderViewModel: ImageLoadingViewModel
    
    init(url: String, key: String) {
        // _ : 加载器  wrappedValue: 包装器
        _loaderViewModel = StateObject(wrappedValue: ImageLoadingViewModel(url: url, key: key))
    }
    
    var body: some View {
        ZStack {
            if loaderViewModel.isLoading{
                ProgressView()
            }else if let image = loaderViewModel.image{
                Image(uiImage: image)
                    .resizable()
                    .clipShape(Circle())
            }
        }
    }
}

struct DownloadingImageView_Previews: PreviewProvider {
    static var previews: some View {
        DownloadingImageView(url: "https://via.placeholder.com/600/92c952", key: "1")
            .frame(width: 75, height: 75)
            .previewLayout(.sizeThatFits)
    }
}

  7.2 创建下载显示图片文字行视图,DownloadingImagesRow.swift

Swift">import SwiftUI

struct DownloadingImagesRow: View {
    let model : PhotoModel
    
    var body: some View {
        HStack {
            DownloadingImageView(url: model.url, key: "\(model.id)")
                .frame(width: 75, height: 75)
            
            VStack (alignment: .leading){
                Text(model.title)
                    .font(.headline)
                Text(model.url)
                    .foregroundColor(.gray)
                    .italic()
            }
            .frame( maxWidth: .infinity, alignment: .leading)
        }
    }
}

struct DownloadingImagesRow_Previews: PreviewProvider {
    static var previews: some View {
        DownloadingImagesRow(model: PhotoModel(albumId: 1, id: 1, title: "title", url: "https://via.placeholder.com/600/92c952", thumbnailUrl: "thumbnaolUrl here"))
            .padding()
            .previewLayout(.sizeThatFits)
    }
}

  7.3 创建下载显示图片文字列表视图,DownloadingImagesBootcamp.swift

Swift">import SwiftUI

// Codable : 可编/解码 JSON 数据
// background threads : 后台线程
// weak self : 弱引用
// Combine : 取消器/组合操作
// Publishers and Subscribers : 发布者与订阅者
// FileManager : 文件管理器
// NSCache : 缓存

struct DownloadingImagesBootcamp: View {
    @StateObject var viewModel = DownloadingImageViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.dataArray) { model in
                   DownloadingImagesRow(model: model)
                }
            }
            .navigationTitle("Downloading Images")
        }
    }
}

struct DownloadingImagesBootcamp_Previews: PreviewProvider {
    static var previews: some View {
        DownloadingImagesBootcamp()
    }
}

8. 效果图:


http://www.niftyadmin.cn/n/5074680.html

相关文章

【2020年10月自考】网络设计与组网

机密★启用前 编号: 317 2020年10月云南省高等教育自学考试 网络设计与组网 试卷 (课程代码03948) 注意事项: 1.本试卷分为两部分,第一部分为选择题,第二部分为非选择题。 2.应考者必须按试题顺序在答题卡(纸)指定位置上作答,答在试卷上无效。 3.涂写部分、画图部分…

LeetCode-343-整数拆分

题目描述&#xff1a; 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 题目链接&#xff1a; LeetCode-343-整数拆分 解题思路&#xff1a; 还是根据动规五…

山西电力市场日前价格预测【2023-10-09】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-10-09&#xff09;山西电力市场全天平均日前电价为575.84元/MWh。其中&#xff0c;最高日前电价为1500.00元/MWh&#xff0c;预计出现在17: 30-20: 00。最低日前电价为218.27元/MWh&#x…

freertos信号量之递归互斥信号量

freertos信号量之递归互斥信号量 简介例程 简介 递归互斥信号量&#xff08;Recursive Mutex Semaphore&#xff09;是一种特殊类型的互斥信号量&#xff0c;允许持有互斥信号量的任务多次重新获取这个互斥信号量。以下是递归互斥信号量的常用函数及其说明&#xff1a; 1&…

【Spring MVC研究】MVC如何浏览器请求(service方法)

文章目录 1. DispatcherServlet 的 service 方法1.1. processRequest 方法1.2. doService 方法 背景&#xff1a;平时我们学习 MVC 重点关注的时DispatcherServlet 的 doDispatcher 方法&#xff0c;但是在 doDispatcher 方法之前 还有请求处理的前置过程&#xff0c;这个过程…

Java IO 流:简介与代码示例

Java IO 流&#xff1a;简介与代码示例 在 Java 中&#xff0c;IO&#xff08;Input/Output&#xff09;流是处理输入和输出的核心工具。通过 IO 流&#xff0c;我们可以读取和写入各种数据类型&#xff0c;包括文本、图片、音频等等。在本文中&#xff0c;我们将简要介绍 Jav…

Spring源码解析(十一):spring事务配置类源码

Spring源码系列文章 Spring源码解析(一)&#xff1a;环境搭建 Spring源码解析(二)&#xff1a;bean容器的创建、默认后置处理器、扫描包路径bean Spring源码解析(三)&#xff1a;bean容器的刷新 Spring源码解析(四)&#xff1a;单例bean的创建流程 Spring源码解析(五)&…

Linux手记

常用的配置文件 文件作用/etc/profile系统级别的shell配置文件&#xff0c;它包含了系统中所有用户的默认环境变量和系统级别的全局配置信息/etc/apt/apt.conf配置APT&#xff08;Advanced Package Tool&#xff09;软件包管理器的行为&#xff0c;包括代理等/etc/apt/sources…