Просмотр исходного кода

随着项目对库进行完善

100Years 4 недель назад
Родитель
Сommit
4db657260b

+ 7 - 3
TSSmalCoacopods/Classes/BaseClass/TSBaseNavigationBarView.swift

@@ -152,10 +152,14 @@ open class TSNormalNavigationBarView: TSBaseNavContentBarView {
 
     public func setNavButton(button: UIButton, name: String, imageName: String, target: Any?, action: Selector) {
         button.setTitle(name, for: .normal)
-        let image = UIImage(bundleNamed: imageName)?.withRenderingMode(.alwaysOriginal)
-        if let image = image {
-            button.setImage(image.mirror, for: .normal)
+        
+        if imageName.count > 0 {
+            let image = UIImage(bundleNamed: imageName)?.withRenderingMode(.alwaysOriginal)
+            if let image = image {
+                button.setImage(image.mirror, for: .normal)
+            }
         }
+
         button.addTarget(target, action: action, for: .touchUpInside)
     }
 

+ 6 - 6
TSSmalCoacopods/Classes/BaseClass/TSBaseVC.swift

@@ -42,11 +42,11 @@ open class TSBaseVC: UIViewController {
         return view
     }()
     
-    public lazy var nullView: UIView = {
-        let view = UIView()
-        view.backgroundColor = .clear
-        return view
-    }()
+//    public lazy var nullView: UIView = {
+//        let view = UIView()
+//        view.backgroundColor = .clear
+//        return view
+//    }()
     
     public lazy var netWorkView: UIView = {
         let view = UIView()
@@ -207,7 +207,7 @@ open class TSBaseVC: UIViewController {
         }
     }
 
-    @objc public func navBarClickLeftAction() {
+    @objc open func navBarClickLeftAction() {
         debugPrint("navBarClickLeftAction -> \(type(of: self))")
         pop()
     }

+ 16 - 0
TSSmalCoacopods/Classes/Ex/Dictionary+Ex.swift

@@ -43,3 +43,19 @@ public extension Dictionary where Key == String {
         return valueT
     }
 }
+
+public extension Dictionary where Key == String {
+    /// 将字典转换为 JSON 字符串
+    func toJSONString() -> String? {
+        do {
+            // 将字典转换为 JSON 数据
+            let jsonData = try JSONSerialization.data(withJSONObject: self, options: .prettyPrinted)
+            
+            // 将 JSON 数据转换为字符串
+            return String(data: jsonData, encoding: .utf8)
+        } catch {
+            print("Error converting dictionary to JSON: \(error)")
+            return nil
+        }
+    }
+}

+ 5 - 0
TSSmalCoacopods/Classes/Ex/Int+Ex.swift

@@ -55,4 +55,9 @@ public extension Int {
         // 格式化为 "HH:mm:ss"
         return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
     }
+    
+    static func timestampInt() -> Int {
+         let currentTimestamp = Int(Date().timeIntervalSince1970)
+         return currentTimestamp
+     }
 }

+ 21 - 0
TSSmalCoacopods/Classes/Ex/SwiftUI/Text+Ex.swift

@@ -0,0 +1,21 @@
+//
+//  Text+Ex.swift
+//  Pods
+//
+//  Created by 100Years on 2025/3/13.
+//
+
+import SwiftUI
+public extension Text {
+    func gradientForeground(colors: [Color], startPoint: UnitPoint, endPoint: UnitPoint) -> some View {
+        self
+            .overlay(
+                LinearGradient(
+                    gradient: Gradient(colors: colors),
+                    startPoint: startPoint,
+                    endPoint: endPoint
+                )
+                .mask(self)
+            )
+    }
+}

+ 13 - 1
TSSmalCoacopods/Classes/Ex/UIButton+Ex.swift

@@ -108,9 +108,21 @@ public extension UIButton {
     @objc private func buttonTapped() {
         actionClosure?()
     }
-}
+    
+    
+    
 
+}
 
+public extension UIButton {
+    
+    public func setTitleImageSpace(spacing:CGFloat = 4){
+//        titleEdgeInsets = UIEdgeInsets(top: 0, left: spacing, bottom: 0, right: 0) // 只调整 title 的 left
+        contentEdgeInsets = UIEdgeInsets(top: 0, left: spacing, bottom: 0, right: 0) // 只调整 title 的 left
+        imageEdgeInsets = UIEdgeInsets(top: 0, left: -spacing, bottom: 0, right: 0) // 只调整 image 的 right
+    }
+    
+}
 public class TSUIExpandedTouchButton: UIButton {
     public var indexPath: IndexPath = IndexPath(item: 0, section: 0)
     

+ 10 - 4
TSSmalCoacopods/Classes/Ex/UIImageView+Ex.swift

@@ -40,7 +40,11 @@ public extension UIImageView {
                                 backgroundColor: UIColor = .clear,
                                 corner: CGFloat = 0.0) -> UIImageView {
         let imageView = UIImageView()
-        imageView.image = UIImage(named: imageName)
+
+        if imageName.count > 0 {
+            imageView.image = UIImage(named: imageName)
+        }
+        
         imageView.contentMode = contentMode
         imageView.backgroundColor = backgroundColor
         imageView.cornerRadius = corner
@@ -89,13 +93,17 @@ public extension UIImageView {
             return
         }
       
+        if urlString.count == 0 {
+            completion?(nil)
+            return
+        }
+        
         if urlString.contains("http") {
             guard let url = URL(string: urlString) else {
                 completion?(nil)
                 return
             }
             kf.indicatorType = showLoading ? .custom(indicator: TSCustomActivityIndicator(color: .white)) : .none
-
             imageView.kf.setImage(with: url,
                  placeholder: placeholder,
                      options: nil,
@@ -111,10 +119,8 @@ public extension UIImageView {
             }
             
         }else if urlString.contains("/") {
-            imageView.image = placeholder
             imageView.image = UIImage(contentsOfFile: urlString.fillCachePath)
         }else {
-            imageView.image = placeholder
             if let image = UIImage(named: urlString) {
                 imageView.image = image
             }

+ 1 - 0
TSSmalCoacopods/Classes/Ex/UIView+Ex.swift

@@ -26,6 +26,7 @@ public extension UIView {
         gl.startPoint = startPoint
         gl.endPoint = endPoint
         gl.frame = CGRect(origin: .zero, size: layerSize ?? self.bounds.size)
+        gl.zPosition =  -1 // 将 gradientLayer 放在底层
         layer.insertSublayer(gl, at: 0)
     }
     

+ 1 - 1
TSSmalCoacopods/Classes/Tool/SVProgressHUD+Ex.swift

@@ -13,7 +13,7 @@ extension SVProgressHUD {
     @objc dynamic func swizzledMoveToPoint(_ newCenter: CGPoint, rotateAngle angle: CGFloat) {
         // 调用原方法
         self.swizzledMoveToPoint(newCenter, rotateAngle: angle)
-        print("swizzledMoveToPoint swizzledMoveToPoint")
+//        print("swizzledMoveToPoint swizzledMoveToPoint")
         // 使用运行时获取 hudView 属性
         if let hudView = getHudView() {
             if let containerView = containerView {

+ 97 - 0
TSSmalCoacopods/Classes/Tool/TSCommonTool/TSCommonTool+Down.swift

@@ -0,0 +1,97 @@
+//
+//  TSCommonTool+Down.swift
+//  Pods
+//
+//  Created by 100Years on 2025/3/17.
+//
+
+public extension TSCommonTool {
+    
+    /// 下载并缓存文件,依据 URL 的后缀名动态设置文件名
+    /// - Parameters:
+    ///   - url: 文件的 URL 地址
+    ///   - completion: 完成回调,返回本地缓存路径或错误
+    public static func downloadAndCacheFile(from urlString: String, fileEx:String? = nil, cacheDirectory:String = "cacheAll",completion: @escaping (String?, Error?) -> Void) {
+        
+        guard let url = URL(string: urlString) else{
+            completion(nil, NSError(domain: "url null", code: 0))
+            return
+        }
+        
+        
+        if !urlString.contains("http") && urlString.contains("/"){
+            completion(urlString.fillCachePath, nil)
+            return
+        }
+        
+        let fileManager = FileManager.default
+        
+        // 获取缓存目录下的 `cacheVideo` 文件夹路径
+        let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
+        let cacheVideoDirectory = cachesDirectory.appendingPathComponent(cacheDirectory)
+        
+        // 创建 `cacheVideo` 文件夹(如果不存在)
+        if !fileManager.fileExists(atPath: cacheVideoDirectory.path) {
+            do {
+                try fileManager.createDirectory(at: cacheVideoDirectory, withIntermediateDirectories: true, attributes: nil)
+            } catch {
+                completion(nil, error)
+                return
+            }
+        }
+        
+        var fileName = url.path.md5
+        
+        // 使用 URL 的 MD5 哈希值作为缓存文件名,附加 URL 的后缀名
+        var fileExtension = fileEx
+        fileExtension = fileExtension ?? (url.pathExtension.isEmpty ? "" : url.pathExtension)
+        if let fileExtension = fileExtension,fileExtension.count > 0 {
+            fileName = url.path.md5 + ".\(fileExtension)"
+        }
+
+        let cachedFileURL = cacheVideoDirectory.appendingPathComponent(fileName)
+        
+        //检查文件是否已存在于缓存中
+        if fileManager.fileExists(atPath: cachedFileURL.path) {
+            print("文件已存在于缓存中: \(cachedFileURL)")
+            completion(cachedFileURL.path, nil)
+            return
+        }
+        
+        // 下载文件
+        let task = URLSession.shared.downloadTask(with: url) { tempFileURL, response, error in
+            if let error = error {
+                DispatchQueue.main.async {
+                    completion(nil, error)
+                }
+                return
+            }
+            
+            guard let tempFileURL = tempFileURL else {
+                DispatchQueue.main.async {
+                    completion(nil, NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "临时文件路径不存在"]))
+                }
+                return
+            }
+            
+            do {
+                if fileManager.fileExists(atPath: cachedFileURL.path) {
+                    try fileManager.removeItem(atPath:cachedFileURL.path)
+                    dePrint("下载成功,移除已有的旧文件: \(cachedFileURL)")
+                }
+                try fileManager.moveItem(at: tempFileURL, to: cachedFileURL)
+                dePrint("文件下载并缓存成功: \(cachedFileURL)")
+                DispatchQueue.main.async {
+                    completion(cachedFileURL.path, nil)
+                }
+            } catch {
+                dePrint("文件下载成功,但失败:\(error)")
+                DispatchQueue.main.async {
+                    completion(nil, error)
+                }
+            }
+        }
+        
+        task.resume()
+    }
+}

+ 259 - 0
TSSmalCoacopods/Classes/Tool/TSCommonTool/TSCommonTool+MultDown.swift

@@ -0,0 +1,259 @@
+//
+//  MultiTaskDown.swift
+//  Pods
+//
+//  Created by 100Years on 2025/3/17.
+//
+
+public extension TSCommonTool {
+    
+    //    /// 多任务下载并缓存文件,依据 URL 的后缀名动态设置文件名
+    //    /// - Parameters:
+    //    ///   - url: 文件的 URL 地址
+    //    ///   - completion: 完成回调,返回本地缓存路径或错误
+    //    public static func multidownloadAndCacheFile(
+    //        from urlString: String,
+    //        fileEx:String? = nil,
+    //        cacheDirectory:String = "cacheAll",
+    //        progressHandler: @escaping (Double) -> Void,
+    //        completion: @escaping (String?, Error?) -> Void) -> TSMultiTaskDownloader?
+    //    {
+    //
+    //        guard let url = URL(string: urlString) else{
+    //            completion(nil, NSError(domain: "url null", code: 0))
+    //            return nil
+    //        }
+    //
+    //
+    //        if !urlString.contains("http") && urlString.contains("/"){
+    //            completion(urlString.fillCachePath, nil)
+    //            return nil
+    //        }
+    //
+    //        let fileManager = FileManager.default
+    //
+    //        // 获取缓存目录下的 `cacheVideo` 文件夹路径
+    //        let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
+    //        let cacheVideoDirectory = cachesDirectory.appendingPathComponent(cacheDirectory)
+    //
+    //        // 创建 `cacheVideo` 文件夹(如果不存在)
+    //        if !fileManager.fileExists(atPath: cacheVideoDirectory.path) {
+    //            do {
+    //                try fileManager.createDirectory(at: cacheVideoDirectory, withIntermediateDirectories: true, attributes: nil)
+    //            } catch {
+    //                completion(nil, error)
+    //                return nil
+    //            }
+    //        }
+    //
+    //        var fileName = url.path.md5
+    //
+    //        // 使用 URL 的 MD5 哈希值作为缓存文件名,附加 URL 的后缀名
+    //        var fileExtension = fileEx
+    //        fileExtension = fileExtension ?? (url.pathExtension.isEmpty ? "" : url.pathExtension)
+    //        if let fileExtension = fileExtension,fileExtension.count > 0 {
+    //            fileName = url.path.md5 + ".\(fileExtension)"
+    //        }
+    //
+    //        let cachedFileURL = cacheVideoDirectory.appendingPathComponent(fileName)
+    //
+    //        //检查文件是否已存在于缓存中
+    //        if fileManager.fileExists(atPath: cachedFileURL.path) {
+    //            print("文件已存在于缓存中: \(cachedFileURL)")
+    //            completion(cachedFileURL.path, nil)
+    //            return nil
+    //        }
+    //
+    //
+    //        let downloader = TSMultiTaskDownloader.shared
+    //
+    //        let url1 = URL(string: "https://example.com/file1.zip")!
+    //        let url2 = URL(string: "https://example.com/file2.zip")!
+    //
+    //        downloader.downloadFile(from: url, progressHandler: { progress in
+    //            print("Download progress for file1: \(progress * 100)%")
+    //            progressHandler(progress)
+    //        }, completionHandler: { tempFileURL, error in
+    //            if let error = error {
+    //                DispatchQueue.main.async {
+    //                    completion(nil, error)
+    //                }
+    //                return
+    //            }
+    //
+    //            guard let tempFileURL = tempFileURL else {
+    //                DispatchQueue.main.async {
+    //                    completion(nil, NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "临时文件路径不存在"]))
+    //                }
+    //                return
+    //            }
+    //
+    //            do {
+    //                if fileManager.fileExists(atPath: cachedFileURL.path) {
+    //                    try fileManager.removeItem(atPath:cachedFileURL.path)
+    //                    dePrint("下载成功,移除已有的旧文件: \(cachedFileURL)")
+    //                }
+    //                try fileManager.moveItem(at: tempFileURL, to: cachedFileURL)
+    //                dePrint("文件下载并缓存成功: \(cachedFileURL)")
+    //                DispatchQueue.main.async {
+    //                    completion(cachedFileURL.path, nil)
+    //                }
+    //            } catch {
+    //                dePrint("文件下载成功,但失败:\(error)")
+    //                DispatchQueue.main.async {
+    //                    completion(nil, error)
+    //                }
+    //            }
+    //        })
+    //
+    //        return downloader
+    //    }
+    
+    /// 多任务下载并缓存文件,依据 URL 的后缀名动态设置文件名
+    /// - Parameters:
+    ///   - url: 文件的 URL 地址
+    ///   - completion: 完成回调,返回本地缓存路径或错误
+    public static func multidownloadAndCacheFile(
+        from urlString: String,
+        fileEx:String? = nil,
+        cacheDirectory:String = "cacheAll",
+        progressHandler: @escaping (Double) -> Void,
+        completion: @escaping (String?, Error?) -> Void) -> TSMultiTaskDownloader?
+    {
+        
+        guard let url = URL(string: urlString) else{
+            completion(nil, NSError(domain: "url null", code: 0))
+            return nil
+        }
+        
+    
+        guard let cachedFileURL = checkURLString(
+            from: urlString,
+            fileEx: fileEx,
+            cacheDirectory: cacheDirectory,
+            progressHandler: progressHandler,
+            completion: completion
+        ) else {
+            return nil
+        }
+        
+        let fileManager = FileManager.default
+        //检查文件是否已存在于缓存中
+        if fileManager.fileExists(atPath: cachedFileURL.path) {
+            print("文件已存在于缓存中: \(cachedFileURL)")
+            completion(cachedFileURL.path, nil)
+            return nil
+        }
+        
+        
+        let downloader = TSMultiTaskDownloader.shared
+        downloader.downloadFile(from: url, progressHandler: { progress in
+            print("Download progress for file1: \(progress * 100)%")
+            progressHandler(progress)
+        }, completionHandler: { tempFileURL, error in
+            if let error = error {
+                DispatchQueue.main.async {
+                    completion(nil, error)
+                }
+                return
+            }
+            
+            guard let tempFileURL = tempFileURL else {
+                DispatchQueue.main.async {
+                    completion(nil, NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "临时文件路径不存在"]))
+                }
+                return
+            }
+            
+            do {
+                if fileManager.fileExists(atPath: cachedFileURL.path) {
+                    try fileManager.removeItem(atPath:cachedFileURL.path)
+                    dePrint("下载成功,移除已有的旧文件: \(cachedFileURL)")
+                }
+                try fileManager.moveItem(at: tempFileURL, to: cachedFileURL)
+                dePrint("文件下载并缓存成功: \(cachedFileURL)")
+                DispatchQueue.main.async {
+                    completion(cachedFileURL.path, nil)
+                }
+            } catch {
+                dePrint("文件下载成功,但失败:\(error)")
+                DispatchQueue.main.async {
+                    completion(nil, error)
+                }
+            }
+        })
+        
+        return downloader
+    }
+    
+    //检查 url 对不对
+    public static func checkURLString(
+        from urlString: String,
+        fileEx:String? = nil,
+        cacheDirectory:String = "cacheAll",
+        progressHandler:((Double) -> Void)? = nil,
+        completion:((String?, Error?) -> Void)? = nil
+    )->URL?
+    {
+        guard let url = URL(string: urlString) else{
+            completion?(nil, NSError(domain: "url null", code: 0))
+            return nil
+        }
+        
+        
+        if !urlString.contains("http") && urlString.contains("/"){
+            completion?(urlString.fillCachePath, nil)
+            return nil
+        }
+        
+        let fileManager = FileManager.default
+        
+        // 获取缓存目录下的 `cacheVideo` 文件夹路径
+        let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
+        let cacheVideoDirectory = cachesDirectory.appendingPathComponent(cacheDirectory)
+        
+        // 创建 `cacheVideo` 文件夹(如果不存在)
+        if !fileManager.fileExists(atPath: cacheVideoDirectory.path) {
+            do {
+                try fileManager.createDirectory(at: cacheVideoDirectory, withIntermediateDirectories: true, attributes: nil)
+            } catch {
+                completion?(nil, error)
+                return nil
+            }
+        }
+        
+        var fileName = url.path.md5
+        
+        // 使用 URL 的 MD5 哈希值作为缓存文件名,附加 URL 的后缀名
+        var fileExtension = fileEx
+        fileExtension = fileExtension ?? (url.pathExtension.isEmpty ? "" : url.pathExtension)
+        if let fileExtension = fileExtension,fileExtension.count > 0 {
+            fileName = url.path.md5 + ".\(fileExtension)"
+        }
+        
+        let cachedFileURL = cacheVideoDirectory.appendingPathComponent(fileName)
+        return cachedFileURL
+
+    }
+    
+    //获取 urlstring 本地的缓存 url path
+    public static func getCachedURLString(
+        from urlString: String,
+        fileEx:String? = nil,
+        cacheDirectory:String = "cacheAll")->URL?{
+        
+        if let cachedFileURL = checkURLString(
+            from: urlString,
+            fileEx: fileEx,
+            cacheDirectory: cacheDirectory
+        ){
+            //检查文件是否已存在于缓存中
+            if FileManager.default.fileExists(atPath: cachedFileURL.path) {
+                print("文件已存在于缓存中: \(cachedFileURL)")
+                return cachedFileURL
+            }
+        }
+        
+        return nil
+    }
+}

+ 5 - 89
TSSmalCoacopods/Classes/Tool/TSCommonTool/TSCommonTool.swift

@@ -99,98 +99,14 @@ open class TSCommonTool {
             }
         }
     }
-    
-    
-    
-    /// 下载并缓存文件,依据 URL 的后缀名动态设置文件名
-    /// - Parameters:
-    ///   - url: 文件的 URL 地址
-    ///   - completion: 完成回调,返回本地缓存路径或错误
-    public static func downloadAndCacheFile(from urlString: String, fileEx:String? = nil, cacheDirectory:String = "cacheAll",completion: @escaping (String?, Error?) -> Void) {
-        
-        guard let url = URL(string: urlString) else{
-            completion(nil, NSError(domain: "url null", code: 0))
-            return
-        }
-        
-        
-        if !urlString.contains("http") && urlString.contains("/"){
-            completion(urlString.fillCachePath, nil)
-            return
-        }
-        
-        let fileManager = FileManager.default
-        
-        // 获取缓存目录下的 `cacheVideo` 文件夹路径
-        let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
-        let cacheVideoDirectory = cachesDirectory.appendingPathComponent(cacheDirectory)
-        
-        // 创建 `cacheVideo` 文件夹(如果不存在)
-        if !fileManager.fileExists(atPath: cacheVideoDirectory.path) {
-            do {
-                try fileManager.createDirectory(at: cacheVideoDirectory, withIntermediateDirectories: true, attributes: nil)
-            } catch {
-                completion(nil, error)
-                return
-            }
-        }
-        
-        var fileName = url.path.md5
-        
-        // 使用 URL 的 MD5 哈希值作为缓存文件名,附加 URL 的后缀名
-        var fileExtension = fileEx
-        fileExtension = fileExtension ?? (url.pathExtension.isEmpty ? "" : url.pathExtension)
-        if let fileExtension = fileExtension,fileExtension.count > 0 {
-            fileName = url.path.md5 + ".\(fileExtension)"
-        }
 
-        let cachedFileURL = cacheVideoDirectory.appendingPathComponent(fileName)
-        
-        //检查文件是否已存在于缓存中
-//        if fileManager.fileExists(atPath: cachedFileURL.path) {
-//            print("文件已存在于缓存中: \(cachedFileURL)")
-//            completion(cachedFileURL.path, nil)
-//            return
-//        }
-        
-        // 下载文件
-        let task = URLSession.shared.downloadTask(with: url) { tempFileURL, response, error in
-            if let error = error {
-                DispatchQueue.main.async {
-                    completion(nil, error)
-                }
-                return
-            }
-            
-            guard let tempFileURL = tempFileURL else {
-                DispatchQueue.main.async {
-                    completion(nil, NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "临时文件路径不存在"]))
-                }
-                return
-            }
-            
-            do {
-                if fileManager.fileExists(atPath: cachedFileURL.path) {
-                    try fileManager.removeItem(atPath:cachedFileURL.path)
-                    dePrint("下载成功,移除已有的旧文件: \(cachedFileURL)")
-                }
-                try fileManager.moveItem(at: tempFileURL, to: cachedFileURL)
-                dePrint("文件下载并缓存成功: \(cachedFileURL)")
-                DispatchQueue.main.async {
-                    completion(cachedFileURL.path, nil)
-                }
-            } catch {
-                dePrint("文件下载成功,但失败:\(error)")
-                DispatchQueue.main.async {
-                    completion(nil, error)
-                }
-            }
-        }
-        
-        task.resume()
-    }
 }
 
+
+
+
+
+
 public let kMainQueue = DispatchQueue.main
 /// 主线程延迟执行回调
 /// - Parameters:

+ 83 - 0
TSSmalCoacopods/Classes/Tool/TSCommonTool/TSMultiTaskDownloader.swift

@@ -0,0 +1,83 @@
+//
+//  TSMultiTaskDownloader.swift
+//  Pods
+//
+//  Created by 100Years on 2025/3/17.
+//
+
+import Foundation
+
+public class TSMultiTaskDownloader: NSObject, URLSessionDownloadDelegate {
+    
+    public static let shared = TSMultiTaskDownloader()
+    
+    private var session: URLSession!
+    private var downloadTasks: [URLSessionDownloadTask] = []
+    private var progressHandlers: [URL: (Double) -> Void] = [:]
+    private var completionHandlers: [URL: (URL?, Error?) -> Void] = [:]
+    
+    override init() {
+        super.init()
+        let config = URLSessionConfiguration.background(withIdentifier: "com.yourapp.multitaskdownloader")
+        session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
+    }
+    
+    public func downloadFile(from url: URL, progressHandler: @escaping (Double) -> Void, completionHandler: @escaping (URL?, Error?) -> Void) {
+        let downloadTask = session.downloadTask(with: url)
+        downloadTasks.append(downloadTask)
+        progressHandlers[url] = progressHandler
+        completionHandlers[url] = completionHandler
+        downloadTask.resume()
+    }
+    
+    public func cancelDownload(for url: URL) {
+        if let task = downloadTasks.first(where: { $0.originalRequest?.url == url }) {
+            task.cancel()
+            downloadTasks.removeAll { $0.originalRequest?.url == url }
+            progressHandlers.removeValue(forKey: url)
+            completionHandlers.removeValue(forKey: url)
+        }
+    }
+    
+    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
+        guard let originalURL = downloadTask.originalRequest?.url else { return }
+        
+        let fileManager = FileManager.default
+        let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+        let destinationURL = documentsDirectory.appendingPathComponent(originalURL.lastPathComponent)
+        
+        do {
+            try fileManager.moveItem(at: location, to: destinationURL)
+            completionHandlers[originalURL]?(destinationURL, nil)
+        } catch {
+            completionHandlers[originalURL]?(nil, error)
+        }
+        
+        downloadTasks.removeAll { $0.originalRequest?.url == originalURL }
+        progressHandlers.removeValue(forKey: originalURL)
+        completionHandlers.removeValue(forKey: originalURL)
+    }
+    
+    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
+        guard let originalURL = downloadTask.originalRequest?.url else { return }
+        
+        let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
+        progressHandlers[originalURL]?(progress)
+    }
+    
+    public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+        guard let originalURL = task.originalRequest?.url else { return }
+        
+        if let error = error {
+            completionHandlers[originalURL]?(nil, error)
+        }
+        
+        downloadTasks.removeAll { $0.originalRequest?.url == originalURL }
+        progressHandlers.removeValue(forKey: originalURL)
+        completionHandlers.removeValue(forKey: originalURL)
+    }
+    
+    deinit {
+        session.invalidateAndCancel()
+    }
+}

+ 10 - 6
TSSmalCoacopods/Classes/Tool/TSToastTool.swift

@@ -24,6 +24,10 @@ open class TSToastTool {
         SVProgressHUD.setCornerRadius(16.0)
         SVProgressHUD.setBackgroundColor("#000000".uiColor.withAlphaComponent(0.8))
         SVProgressHUD.setForegroundColor(.white)
+        SVProgressHUD.setMinimumDismissTimeInterval(3.0)
+//        SVProgressHUD.setInfoImage(UIImage())
+//        SVProgressHUD.setImageViewSize(CGSize.zero)
+
     }
     
     
@@ -47,7 +51,10 @@ open class TSToastTool {
             kExecuteOnMainThread {
                 if let containerView = containerView {
                     SVProgressHUD.setContainerView(containerView)
+                }else if let window = Self.getCurrentWindow() {
+                    SVProgressHUD.setContainerView(window)
                 }
+                SVProgressHUD.setDefaultMaskType(.none)
                 SVProgressHUD.showInfo(withStatus: text)
             }
         }
@@ -57,7 +64,7 @@ open class TSToastTool {
     /// 显示加载动画
     public func showLoading(text:String? = nil,containerView:UIView?) {
         kExecuteOnMainThread {
-//            SVProgressHUD.dismiss()
+            SVProgressHUD.setDefaultMaskType(.clear)
             SVProgressHUD.setContainerView(containerView)
             SVProgressHUD.show(withStatus: text)
         }
@@ -73,11 +80,8 @@ open class TSToastTool {
     /// 显示进度提示
     public func showProgress(progress:Float, status: String?,containerView:UIView?) {
         kExecuteOnMainThread {
-            
-//            if let containerView = containerView {
-                SVProgressHUD.setContainerView(containerView)
-//            }
-            
+            SVProgressHUD.setDefaultMaskType(.clear)
+            SVProgressHUD.setContainerView(containerView)
             SVProgressHUD.showProgress(progress, status: status)
         }
     }

+ 6 - 6
TSSmalCoacopods/Classes/Tool/WindowHelper.swift

@@ -7,10 +7,10 @@
 
 import UIKit
 
-class WindowHelper {
+open class WindowHelper {
     
     /// 获取当前的 keyWindow
-    static func getKeyWindow() -> UIWindow? {
+    public static func getKeyWindow() -> UIWindow? {
         // 在 iOS 13 及以上,SceneDelegate 管理窗口
         if #available(iOS 13.0, *) {
             return UIApplication.shared.connectedScenes
@@ -24,7 +24,7 @@ class WindowHelper {
     }
     
     /// 获取当前窗口,兼容 iOS 13 及以上版本
-    static func getCurrentWindow() -> UIWindow? {
+    public static func getCurrentWindow() -> UIWindow? {
         if #available(iOS 13.0, *) {
             // iOS 13 及以上使用 `scene` 获取当前 window
             guard let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene else {
@@ -38,7 +38,7 @@ class WindowHelper {
     }
     
     /// 获取当前根视图控制器
-    static func getRootViewController() -> UIViewController? {
+    public static func getRootViewController() -> UIViewController? {
         guard let window = getCurrentWindow() else {
             return nil
         }
@@ -46,7 +46,7 @@ class WindowHelper {
     }
     
     /// 获取当前的视图控制器
-    static func getCurrentViewController() -> UIViewController? {
+    public static func getCurrentViewController() -> UIViewController? {
         var currentViewController = getRootViewController()
         
         while let presentedViewController = currentViewController?.presentedViewController {
@@ -57,7 +57,7 @@ class WindowHelper {
     }
     
     
-    static func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
+    public static func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
         if let nav = base as? UINavigationController {
             return topViewController(base: nav.visibleViewController)
         } else if let tab = base as? UITabBarController {

+ 2 - 2
TSSmalCoacopods/Classes/View/TSPlaceholderTextView/TSPlaceholderTextView.swift

@@ -8,7 +8,7 @@
 open class TSPlaceholderTextView: UITextView {
     
     // Placeholder Label
-    lazy var placeholderLabel: TopLeftLabel = {
+    public lazy var placeholderLabel: TopLeftLabel = {
         let placeholderLabel = TopLeftLabel()
         placeholderLabel.font = font
         placeholderLabel.textColor = placeholderColor
@@ -51,7 +51,7 @@ open class TSPlaceholderTextView: UITextView {
             updatePlaceholderVisibility()
         }
     }
-    
+
     // Initializer
     public init(placeholder: String? = nil,
          text: String? = nil,

+ 1 - 1
TSSmalCoacopods/Classes/View/UICollectionView+Component/CollectionViewComponent.swift

@@ -9,7 +9,7 @@ import UIKit
 
 
 
-let kIndexPath = "kIndexPath"
+public let kIndexPath = "kIndexPath"
 
 open class TSCollectionView: UICollectionView {
     lazy var reuseSubviews: [String: UIView] = [:]

+ 4 - 4
TSSmalCoacopods/Classes/View/UILabel/PaddedLabel.swift

@@ -6,7 +6,7 @@
 //
 
 import UIKit
-class TopLeftLabel: UILabel {
+open class TopLeftLabel: UILabel {
     
     // 设置内间距
     var textInsets: UIEdgeInsets = .zero {
@@ -15,7 +15,7 @@ class TopLeftLabel: UILabel {
         }
     }
     
-    override func drawText(in rect: CGRect) {
+    open override func drawText(in rect: CGRect) {
         // 根据内间距调整绘制区域
         let insetRect = rect.inset(by: textInsets)
         
@@ -36,7 +36,7 @@ class TopLeftLabel: UILabel {
         super.drawText(in: adjustedRect)
     }
     
-    override var intrinsicContentSize: CGSize {
+    open override var intrinsicContentSize: CGSize {
         // 根据内容大小和内间距调整固有内容大小
         guard let text = text else { return .zero }
         let textSize = (text as NSString).boundingRect(
@@ -50,7 +50,7 @@ class TopLeftLabel: UILabel {
         return CGSize(width: width, height: height)
     }
     
-    override func sizeThatFits(_ size: CGSize) -> CGSize {
+    open override func sizeThatFits(_ size: CGSize) -> CGSize {
         // 调整 `sizeThatFits` 的结果以包含内间距
         guard let text = text else { return .zero }
         let textSize = (text as NSString).boundingRect(

+ 116 - 0
TSSmalCoacopods/Classes/View/UIStackView/TSCustomStackView.swift

@@ -0,0 +1,116 @@
+//
+//  CustomStackView.swift
+//  TestUIKit
+//
+//  Created by 100Years on 2025/2/24.
+//
+
+import UIKit
+import SnapKit
+
+open class TSCustomStackView: UIView {
+    // 内部的 UIScrollView 和 UIStackView
+    public let scrollView: UIScrollView
+    public let stackView: UIStackView
+    
+    // 开放的属性,用于设置方向和间距
+    public var axis: NSLayoutConstraint.Axis {
+        get {
+            return stackView.axis
+        }
+        set {
+            stackView.axis = newValue
+            updateScrollViewConstraints()
+        }
+    }
+    
+    public var spacing: CGFloat {
+        get {
+            return stackView.spacing
+        }
+        set {
+            stackView.spacing = newValue
+        }
+    }
+    
+    public var viewH:CGFloat {
+        get {
+            return scrollView.contentSize.height
+        }
+    }
+    
+    // 初始化方法
+    public init(axis: NSLayoutConstraint.Axis = .vertical, spacing: CGFloat = 0) {
+        self.scrollView = UIScrollView()
+        self.stackView = UIStackView()
+        self.stackView.axis = axis
+        self.stackView.spacing = spacing
+        self.stackView.alignment = .fill
+        self.stackView.distribution = .fill
+        super.init(frame: .zero)
+        setupUI()
+    }
+    
+    required public init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    // 设置 UI
+    private func setupUI() {
+        // 添加 scrollView
+        addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        // 添加 stackView 到 scrollView
+        scrollView.addSubview(stackView)
+        updateScrollViewConstraints()
+    }
+    
+    // 根据轴方向更新约束
+    private func updateScrollViewConstraints() {
+        stackView.snp.remakeConstraints { make in
+            make.edges.equalToSuperview()
+            
+            // 根据轴方向设置 contentSize
+            if axis == .vertical {
+                make.width.equalTo(scrollView)
+            } else {
+                make.height.equalTo(scrollView)
+            }
+        }
+    }
+    
+    // 动态添加子视图的方法
+    public func addSubviewToStack(_ view: UIView) {
+        stackView.addArrangedSubview(view)
+        // 可以根据需要对子视图进行额外的布局设置
+        view.snp.makeConstraints { make in
+            if axis == .vertical {
+                make.width.equalTo(stackView)
+            } else {
+                make.height.equalTo(stackView)
+            }
+        }
+    }
+    
+    // 在指定位置插入子视图
+    public func insertViewToStack(_ view: UIView, at stackIndex: Int) {
+        stackView.insertArrangedSubview(view, at: stackIndex)
+        // 可以根据需要对子视图进行额外的布局设置
+        view.snp.makeConstraints { make in
+            if axis == .vertical {
+                make.width.equalTo(stackView)
+            } else {
+                make.height.equalTo(stackView)
+            }
+        }
+    }
+    
+    // 移除子视图
+    public func removeViewToStack(_ view: UIView) {
+        stackView.removeArrangedSubview(view)
+        view.removeFromSuperview()
+    }
+}