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

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 文件

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

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

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

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

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

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

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

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

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. 效果图:

猜你喜欢

转载自blog.csdn.net/u011193452/article/details/133685038