1) Codable : 可编/解码 JSON 数据
2) background threads : 后台线程
3) weak self : 弱引用
4) Combine : 取消器/组合操作
5) Publishers and Subscribers : 发布者与订阅者
6) FileManager : 文件管理器
7) NSCache : 缓存
jsonplaceholder
https://jsonplaceholder.typicode.com/
quicktype
https://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<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
- }
- }
- 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)
- }
- }
- 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<AnyCancellable>()
-
- 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<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)
- }
- }
- 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()
- }
- }
