最近在开发一款本地漫画阅读 APP ,需要从 SMB 服务器上加载图片进行阅读,这是前提。
所使用到的库为注明的 SDWebImageSwiftUI ,异步加载图片。以及 AMSMB2 ,SMB 服务器访问库。
最终目的是,根据给定的 smb 图片文件路径,可以在视图上显示图片 View。
明确输入内容
首先先要明确,如果要显示 smb 图片文件,最少需要的信息是什么。
- 首先是文件的地址,例如"smb://192.168.1.1/Share/1.jpg",这是文件的绝对路径 ,从这里就可以提取出一些关键的信息,例如服务器地址 、共享文件夹 、图片文件相对路径
- 然后是服务器的登录信息 ,包含用户名 和密码。(暂不考虑无密码登录的情况)
也就是说,可以用到的信息如下:
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 解释:
requestImage(with:options:context:progress:completed:)
: 此方法用于开始图像加载操作。它接收一个 URL、一组选项、一个上下文、一个进度回调和一个完成回调。你的自定义加载器应该在此方法中实现图像加载逻辑,并通过回调返回结果。canRequestImage(for:)
: 此方法用于判断加载器是否能够处理特定的 URL。一般来说,如果 URL 符合你的加载逻辑(例如,是一个smb://
URL),你应该返回true
。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 解释:
参数:
- url: URL?
- 这是要加载图片的URL。这通常是一个网络URL,但也可以是文件系统的路径。
- options: SDWebImageOptions = []
- 这是一个包含多个枚举值的数组,用于定制图片加载行为。例如,
.retryFailed
表示如果第一次加载失败,则进行重试;.refreshCached
表示即使图片已缓存,也要从网络重新获取。- context: [SDWebImageContextOption : Any]? = nil
- 这是一个键值对的字典,提供额外的上下文信息。这可以用于传递自定义的Caching或者Downloader选项。
- progress: SDImageLoaderProgressBlock? = nil
- 这是一个闭包,用于在图片加载过程中报告进度。该闭包有三个参数:已接收的数据量、预期需要接收的总数据量和源URL。
- 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 关注。