1) Codable : 可编/解码 JSON 数据
2) background threads : 后台线程
3) weak self : 弱引用
4) Combine : 取消器/组合操作
5) Publishers and Subscribers : 发布者与订阅者
6) FileManager : 文件管理器
7) NSCache : 缓存
jsonplaceholderhttps://jsonplaceholder.typicode.com/
quicktypehttps://app.quicktype.io/
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"
}
*/
import Foundation
import Combine
/// 请求数据服务
class PhotoModelDataService{
// 单例模式 Singleton
static let instance = PhotoModelDataService()
// 返回 JSON 数据,解码成模型
@Published var photoModel:[PhotoModel] = []
// 随时取消请求
var cancellables = Set()
// 只能内部实例化,保证一个 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
}
}
import Foundation
import SwiftUI
/// 图片缓存管理器
class PhotoModelCacheManager{
// 单例模式
static let instance = PhotoModelCacheManager()
// 只能内部实例化,保证一个 App 只有一次实例化
private init() {}
// 图片数量缓存,计算型属性
var photoCache: NSCache = {
let cache = NSCache()
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)
}
}
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)
}
}
import Foundation
import Combine
class DownloadingImageViewModel: ObservableObject{
// 数组模型
@Published var dataArray:[PhotoModel] = []
// 请求数据服务
let dataService = PhotoModelDataService.instance
// 取消操作
var cancellables = Set()
init() {
addSubscribers()
}
// 订阅数据
func addSubscribers(){
dataService.$photoModel
.sink {[weak self] returnedPhotoModel in
guard let self = self else { return }
self.dataArray = returnedPhotoModel
}
.store(in: &cancellables)
}
}
import Foundation
import SwiftUI
import Combine
class ImageLoadingViewModel: ObservableObject{
@Published var image: UIImage?
@Published var isLoading: Bool = false
// 取消
var cancellables = Set()
// 缓存管理器
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)
}
}
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)
}
}
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)
}
}
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()
}
}