如何在受 header-based 身份验证保护的 SwiftUI 中显示图像

How to display an image in SwiftUI protected by header-based authentication

我使用 AsyncImage 在我的 SwiftUI 应用程序中渲染远程图像已有一段时间了。

https://developer.apple.com/documentation/swiftui/asyncimage

我现在需要呈现受保护资产的图像 - 我必须在 header 中提供不记名令牌才能下载这些图像。

很明显 AsyncImage 不能胜任这里的工作,因为无论如何都无法自定义请求。

我有一个 ImageLoader 协议;

protocol ImageLoader {
    func loadImage(for url: URL) async throws -> UIImage
}

其中描述了我编写的用于下载远程图像的服务。

如何为视图层次结构中向下嵌套的多个级别的视图提供此服务。

例如;

struct UserAvatarView: View {
    
    private let name: String
    private let initials: String
    private let image: UIImage?
    
    init(name: String, initials: String, image: UIImage?) {
        self.name = name
        self.initials = initials
        self.image = image
    }
    
    var body: some View {
        GeometryReader { proxy in
            ZStack {
                if let image = image {
                    Image(uiImage: image)
                        .resizable()
                        .scaledToFill()
                        .clipped()
                        .aspectRatio(contentMode: .fill)
                        .clipShape(Rectangle())
                        .frame(width: proxy.size.width - 4, height: proxy.size.width - 4)
                        .clipShape(Circle())
                        .padding(2)
                } else {
                    Circle()
                        .fill(Color(AvatarColor.makeColor(forText: name)))
                        .padding(2)
                    
                    Text(initials)
                        .font(Font(UIFont.preferredFont(for: .body, weight: .regular, size: proxy.size.width / 2.55)))
                        .foregroundColor(.white)
                }
                
                Circle()
                    .strokeBorder(Color.white, lineWidth: 2)
            }
        }
        .shadowed()
    }
}

这个视图可以向下嵌套很多层,我已经把它写得尽可能笨了,但在某些时候我需要获取图像并更新这个视图。

这个视图本身应该获取图像吗?

我可以实现接受 ImageLoader 实例的 UserAvatarViewModel - 但我不清楚如何创建具有所需依赖项的模型。

我很想避免单例实例,我不认为视图应该负责创建视图模型,这排除了 Environment 将加载程序向下传递的可能性。

我曾认为最上面的视图负责所有繁重的工作,但后来我可以将属性传递给视图,只是让它们将它们传递给它们的 child 视图,这感觉就像 anti-pattern 再次.

您只需将AsyncImage 组件替换为您自己的组件即可。我创建了这个组件,类似于 AsyncImage 但它接收自定义异步函数来获取图像

struct CustomAsyncImage<Content: View, Placeholder: View>: View {

  @State var uiImage: UIImage?

  let getImage: () async -> UIImage?
  let content: (Image) -> Content
  let placeholder: () -> Placeholder

  init(
      getImage: @escaping () async -> UIImage?,
      @ViewBuilder content: @escaping (Image) -> Content,
      @ViewBuilder placeholder: @escaping () -> Placeholder
  ){
      self.getImage = getImage
      self.content = content
      self.placeholder = placeholder
  }

  var body: some View {
      if let uiImage = uiImage {
          content(Image(uiImage: uiImage))
      }else {
          placeholder()
              .task {
                  self.uiImage = await getImage()
              }
      }
  }
}

这是一个使用示例。

CustomAsyncImage(getImage: getImage) { image in
  image
    .resizable()
    .scaledToFit()
    .frame(maxWidth: .infinity)
} placeholder: {
    Text("PlaceHolder")
}

其中 getImage 是 return UIImage 的异步函数,您可以根据需要创建该函数。

我将向您展示我的功能作为示例。

func getImage() async -> UIImage? {
    guard let url = URL(string: BASE_URL + "cards/5fd36d31937b6b32d47ad9db/attachments/5fd36d31937b6b32d47ad9de/download") else {
        fatalError("Missing URL")
    }
    
    var request = URLRequest(url: url)
    request.addValue("OAuth oauth_consumer_key=\"\(API_KEY)\", oauth_token=\"\(ACCOUNT_TOKEN)\"", forHTTPHeaderField: "Authorization")
    
    do {
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard (response as? HTTPURLResponse)?.statusCode == 200 else { fatalError("Error while fetching attchment") }
        
        return UIImage(data: data)
    } catch {
        return nil
    }
}