【面向 GPT 编程】使用 SDWebImage 加载 SMB 服务器上的图片

最近在开发一款本地漫画阅读 APP ,需要从 SMB 服务器上加载图片进行阅读,这是前提。

所使用到的库为注明的 SDWebImageSwiftUI ,异步加载图片。以及 AMSMB2 ,SMB 服务器访问库。

最终目的是,根据给定的 smb 图片文件路径,可以在视图上显示图片 View。

明确输入内容

首先先要明确,如果要显示 smb 图片文件,最少需要的信息是什么。

  1. 首先是文件的地址,例如"smb://192.168.1.1/Share/1.jpg",这是文件的绝对路径 ,从这里就可以提取出一些关键的信息,例如服务器地址共享文件夹图片文件相对路径
  2. 然后是服务器的登录信息 ,包含用户名密码。(暂不考虑无密码登录的情况)

也就是说,可以用到的信息如下:

swift 复制代码
//正式使用时需要替换为正确的信息
let filePath:String = "smb://192.168.1.1/Share/1.jpg"
let username:String = "username"
let password:String = "password"

明确输入形式

WebImage 本身是支持直接通过网络链接加载并显示图片的,最简单的用法如下:

swift 复制代码
WebImage(url: URL(string: "https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png"))

但是,却并不支持直接从 SMB 服务器下载数据,但是好在其支持自定义 context ,其中就有自定义 imageLoader ,也就是图片加载方式的选项。

在 WebImage 的源码中可以看到

swift 复制代码
/// Create a web image with url, placeholder, custom options and context.
    /// - Parameter url: The image url
    /// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
    /// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
    public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
        self.init(url: url, options: options, context: context, isAnimating: .constant(true))
    }

而 SDWebImageContextOption 其中的一个选项就是

swift 复制代码
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageLoader;

本身还是 OC 的写法,需要转换。总之到最后使用时,是这样的:

swift 复制代码
WebImage(url: , context: [.imageLoader: ])

可以看到,在传入 url 的基础上,还需要额外传入一个 imageLoader 来实现图片数据的加载。

稍微修改一下之前定义的信息,最终理想状态下针对 SMB 服务器上显示图片的 WebImage 的使用方式应该是:

swift 复制代码
//正式使用时需要替换为正确的信息
let fileUrl:URL = URL(string: "smb://192.168.1.1/Share/1.jpg")
let username:String = "username"
let password:String = "password"
WebImage(url: fileUrl, context: [.imageLoader: SMBImageLoader(username: username, password: password)])

这里我定义了一个 SMBImageLoader 方法,需要根据输入的 username 和 password 完成一次 SMB 服务器的访问和数据下载。

接下来的问题就是如何写一个 SMBImageLoader 了

编写数据加载器

由于 SDWebImage 和 AMSMB2 都是比较老的库,所以理论上可以直接用 GPT 辅助完成这部分的代码。

GPT 回复如下:

swift 复制代码
class SMBImageLoader: SDImageLoader {
    let username: String
    let password: String
    
    init(username: String, password: String) {
        self.username = username
        self.password = password
    }
    
    func requestImage(with url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, progress: SDImageLoaderProgressBlock? = nil, completed: SDImageLoaderCompletedBlock? = nil) -> SDWebImageOperation? {
        
        guard let url = url else {
            completed?(nil, nil, .invalidURL, true)
            return nil
        }
        
        do {
            let smb = try AMSMB2(url: url, credential: URLCredential(user: username, password: password, persistence: .forSession))
            smb.connectShare(name: "Share") { (error) in
                if let error = error {
                    completed?(nil, error, .failed, true)
                    return
                }
                
                smb.readFile(atPath: "1.jpg") { (data, error) in
                    if let error = error {
                        completed?(nil, error, .failed, true)
                        return
                    }
                    
                    if let data = data, let image = UIImage(data: data) {
                        completed?(image, nil, .cache, true)
                    }
                }
            }
        } catch {
            completed?(nil, error, .failed, true)
        }
        
        return nil // SDWebImageOperation
    }
    
    func shouldBlockFailedURL(with url: URL, error: Error) -> Bool {
        return true
    }
}

当然不出意外的报了很多错误,不过没关系,这些错误大部分都是因为近几年两个库升级所致。接下来在这个基础上进行修改即可。

1. 继承 NSObject

首先根据编译器的提示:"Cannot declare conformance to 'NSObjectProtocol' in Swift; 'SMBImageLoader' should inherit 'NSObject' instead" ,我加上 NSObject 协议。

GPT 解释 :"继承自 NSObject 主要是为了让 Swift 类与 Objective-C 进行交互或使用 Objective-C 的特性"

2. 补充 canRequestImage 方法

依然是编译器提示,完整的 SDImageLoader 协议需要实现 requestImage、canRequestImage、shouldBlockFailedURL 方法。

GPT 解释

  1. requestImage(with:options:context:progress:completed:): 此方法用于开始图像加载操作。它接收一个 URL、一组选项、一个上下文、一个进度回调和一个完成回调。你的自定义加载器应该在此方法中实现图像加载逻辑,并通过回调返回结果。
  2. canRequestImage(for:) : 此方法用于判断加载器是否能够处理特定的 URL。一般来说,如果 URL 符合你的加载逻辑(例如,是一个 smb:// URL),你应该返回 true
  3. shouldBlockFailedURL(with:error:) : 此方法用于决定是否应该阻止对特定 URL 的后续加载尝试,通常是因为前一个尝试失败了。如果你希望在一个加载失败后阻止对同一 URL 的后续尝试,你应该返回 true

这里需要补充上 canRequestImage 方法,顺便加上对于 URL 是否为 smb 服务器的验证。

swift 复制代码
func canRequestImage(for url: URL?) -> Bool {
    return url?.scheme == "smb"
}

3. requestImage 修改

requestImage 里问题就很多了,基本上需要完全的重写。

在此之前先搞清楚 requestImage 方法中每个参数的含义

GPT 解释:

参数:
  1. url: URL?
    • 这是要加载图片的URL。这通常是一个网络URL,但也可以是文件系统的路径。
  2. options: SDWebImageOptions = []
    • 这是一个包含多个枚举值的数组,用于定制图片加载行为。例如,.retryFailed表示如果第一次加载失败,则进行重试;.refreshCached表示即使图片已缓存,也要从网络重新获取。
  3. context: [SDWebImageContextOption : Any]? = nil
    • 这是一个键值对的字典,提供额外的上下文信息。这可以用于传递自定义的Caching或者Downloader选项。
  4. progress: SDImageLoaderProgressBlock? = nil
    • 这是一个闭包,用于在图片加载过程中报告进度。该闭包有三个参数:已接收的数据量、预期需要接收的总数据量和源URL。
  5. completed: SDImageLoaderCompletedBlock? = nil
    • 这是一个闭包,当图片加载完成时被调用。该闭包有两个参数:加载的UIImage对象和可能出现的Error
返回值:
  • SDWebImageOperation?
    • 返回一个遵循SDWebImageOperation协议的对象,你可以用这个对象来取消图片的加载操作。

这里需要关心的就是三个参数,url、progress、和 completed

具体思路是,在方法内容根据 url 和 SMBImageLoader(username: username, password: password) 中传入的 username、password 来创建一个服务器进行数据请求与下载。下载的数据应该是 Data 格式的,所以需要使用 UIImage 进行图片的初始化,再之后通过 completed 闭包返回 UIImage 。

到这步之后可以要求 GPT 根据所给的信息完成数据请求与下载,不过 AMSMB2 这块我用的比较熟,就直接自己写了。如果有问题可以参考 AMSMB2 官方的例子 SMBClient 类

这里直接给出完整的 SMBImageLoader 文件:

swift 复制代码
//SMBImageLoader.swift
class SMBImageLoader: NSObject, SDImageLoader {
    let username: String
    let password: String
    
    init(username: String, password: String) {
        self.username = username
        self.password = password
    }
    func canRequestImage(for url: URL?) -> Bool {
        return url?.scheme == "smb"
    }
    func requestImage(with url: URL?, options: SDWebImageOptions = [.refreshCached], context: [SDWebImageContextOption : Any]? = nil, progress: SDImageLoaderProgressBlock? = nil, completed: SDImageLoaderCompletedBlock? = nil) -> SDWebImageOperation? {
        guard url != nil else {
            return nil
        }
        
        let (tempServerURL, tempShare, tempPath) = extractURLComponents(from: url!)
        
        guard tempServerURL != nil else {
            return nil
        }
        
        let serverURL = tempServerURL!
        let share = tempShare ?? ""
        let path = tempPath ?? ""
        
        let credential = URLCredential(user: username, password: password, persistence: URLCredential.Persistence.forSession)
        
        if let client = AMSMB2(url: serverURL, credential: credential){
            
            client.connectShare(name: share){ error in
                if let error = error {
                    print("Connection failed: \(error)")
                    completed?(nil, nil, error, true)
                    return
                }
                client.contents(atPath: path, progress: { (receivedBytes, totalBytes) -> Bool in
                    // 报告加载进度
                    progress?(Int(receivedBytes), Int(totalBytes), url)
                    return true // 继续接收数据
                }) { result in
                    switch result{
                    case .success(let data):
                        print("data: \(data)")
                        // 将数据转换为UIImage
                        if let image = UIImage(data: data) {
                            // 调用SDWebImage的完成闭包
                            completed?(image, data, nil, true)
                        } else {
                            let error = NSError(domain: "SMBImageLoader", code: -1, userInfo: [NSLocalizedDescriptionKey: "Data is not a valid image"])
                            completed?(nil, nil, error, true)
                        }
                    case .failure(let error):
                        print("error: \(error)\n\(url?.absoluteString.removingPercentEncoding ?? "")")
                        completed?(nil, nil, error, true)
                    }
                }
            }
        }
        return nil
    }

    func shouldBlockFailedURL(with url: URL, error: Error) -> Bool {
        return false
    }
}

我在这里先通过 url 、username 和 password 初始化了 SMB 客户端 client,然后连接并获取对应路径下的文件。当然这里额外增加了 contents 方法中的一个 progress 方便后续获取进度,如果没有这个需求可以删除。然后或得数据后通过 UIImage 加载,最后通过 completed 方法回调对应的参数。

GPT 解释:

completed闭包接受四个参数:一个可选的UIImage,一个可选的NSData,一个可选的NSError,以及一个Bool类型的finished标志。

这里还有一个方法 extractURLComponents ,是从 URL 中提取出服务器 URL、共享名称和相对路径。 这种简单的方法直接让 GPT 写倒是不会出问题,顺便还能让他把注释也写出来。

这里直接给出:

swift 复制代码
/// Extracts the server URL, share name, and relative path from a given URL.
/// 从给定的 URL 中提取服务器 URL、共享名称和相对路径。
///
/// - Parameters:
///   - url: The URL to be parsed. 待解析的 URL。
///
/// - Returns: A tuple containing the server URL, share name, and relative path.
///            返回一个包含服务器 URL、共享名称和相对路径的元组。
func extractURLComponents(from url: URL) -> (serverURL: URL?, share: String?, path: String?) {
    // Ensure the URL has a scheme and host.
    // 确保 URL 有一个协议(scheme)和主机(host)。
    guard let scheme = url.scheme,
          let host = url.host else {
        return (nil, nil, nil)
    }
    
    // Construct the server URL.
    // 构造服务器 URL。
    let serverURL = URL(string: "\(scheme)://\(host)")
    
    // Split the URL's path into components.
    // 将 URL 的路径(path)分割成多个组件。
    let pathComponents = url.pathComponents
    
    // Ensure there are at least three components: ["/", "share", "remaining path..."]
    // 确保至少有三个组件:["/", "共享名称", "剩余的相对路径..."]
    guard pathComponents.count >= 3 else {
        return (serverURL, nil, nil)
    }
    
    // Extract the share name from the second component.
    // 从第二个组件中提取共享名称。
    let share = pathComponents[1]
    
    // Extract the remaining path from the third component onwards, joined by "/".
    // 从第三个组件开始提取剩余的路径,并使用 "/" 进行连接。
    let path = pathComponents[2...].joined(separator: "/")
    
    return (serverURL, share, path)
}

顺便说一个题外话,不知道 Xcode 是否有类似 github copliot 一样的功能,有的话写这种方法应该快不少,不用在 ChatGPT 和编译器之间复制来复制去了。

回到主题,然后在 ContentView 中使用:

swift 复制代码
let fileUrl = URL(string: "...")
let username = "..."
let password = "..."
WebImage(url: fileUrl, options: [.refreshCached], context: [.imageLoader: SMBImageLoader(username: username, password: password)])
    .resizable()
    .frame(width:120,height:180)

变量的关键部分我隐去了,需要自行替换成可用的值。

最终显示结果一切正常。

后记

可以看到 GPT 目前还是只有在撰写简单方法的时候不会出问题。逻辑一旦变得复杂,或者资料库中的内容不够新时,出错的概率大幅度增加。 不过在这种情况下,通过 GPT 解释代码文档,尤其是比较复杂的文档,倒是一个不错的选择。 在这个例子中,SDWebImage 这个开源库大部分都是由 OC 编写,对于只掌握了 Swift 的我来说非常不友好,此时使用 GPT 对特定部分内容进行讲解就是个不错的选择。


我是一个略懂代码的设计师,目前正在尝试使用人工智能连通设计与技术,欢迎各位 designer 与 coder 关注。

相关推荐
DisonTangor9 小时前
苹果发布iOS 18.2首个公测版:Siri接入ChatGPT、iPhone 16拍照按钮有用了
ios·chatgpt·iphone
- 羊羊不超越 -9 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
2401_865854881 天前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
HackerTom2 天前
iOS用rime且导入自制输入方案
ios·iphone·rime
良技漫谈2 天前
Rust移动开发:Rust在iOS端集成使用介绍
后端·程序人生·ios·rust·objective-c·swift
2401_852403552 天前
高效管理iPhone存储:苹果手机怎么删除相似照片
ios·智能手机·iphone
星际码仔2 天前
【动画图解】是怎样的方法,能被称作是 Flutter Widget 系统的核心?
android·flutter·ios
emperinter2 天前
WordCloudStudio:AI生成模版为您的文字云创意赋能 !
图像处理·人工智能·macos·ios·信息可视化·iphone
关键帧Keyframe2 天前
音视频面试题集锦第 8 期
ios·音视频开发·客户端
pb82 天前
引入最新fluwx2.5.4的时候报错
flutter·ios