본문 바로가기
Swift/swiftUI

[SwiftUI] ScrollView + AsyncImage + LazyLayout 트러블 슈팅

by 마라민초닭발로제 2025. 3. 18.

이번에 SwiftUI + ScrollView + ImageView 쓰면서 겪었던 트러블 슈팅을 정리해 보려고 합니다. 

 

LazyLayout + AsyncImage

LazyLoyout과 AsynImage를 쓰면서 문제가 있었습니다. LazyLayout에 할당된 AsyncImage가 이상하게 동작했습니다. 우리는 LazyLayout을 통해 사용자 화면에 나타난 AsyncImage를 부를 것 입니다. 하지만 AsyncImageloading하다가 에러가 생기거나 연결이 끊긴 경우에 자동으로 AsyncImage가 처리할 것으로 생각했지만 실제로는 그러지 않았습니다. 그리고 내부 URLSession.shared를 사용하여 이미지를 받아오기에, Cache정책이나 Detail한 부분에 대해서 설정하기 어려웠습니다. 

 

private func makeScrollColumnView(_ imageModel: [ImageModel]) -> some View {
    ForEach(imageModel) { item in
      let selectedItem = selectedImageModel.first { $0 == item }

      AsyncImage(url: item.thumbnailURL) { phase in
        switch phase {
        case .empty:
          ProgressView()
        case let .success(image):
          image.resizable().aspectRatio(contentMode: .fill)
        case .failure(let error):
          Image(systemName: "x.circle.fill").resizable().aspectRatio(contentMode: .fill)
        @unknown default:
          Image(systemName: "x.circle").resizable().aspectRatio(contentMode: .fill)
        }
      }

 

 

 

 

그래서 CustomAsyncImageView를 만들어서 활용했습니다. Data -> UIImage -> Image 로 바꾸는 데이터 흐름이 그렇게 좋지는 않지만, 다른 방법이 생각나지 않아 다음과 같은 방식으로 해결했습니다. 또한 ImageNetworkProvider 의 객체를 만들어 이미지를 관리하는 특정한 Network Module로 이미지 캐시를 관리했습니다. 

struct CustomImageView: URL {
  let url: URL
  @State private var imageData: Data? = nil
  @State private var isErrorOccurred: Bool = false
  var tapped: (_ now: FrommyImageStatus) -> Void

  init(_ url: URL) {
  	self.url = url
  }

  var body: some View {
    makeContent()
      .frame(maxWidth: .infinity, maxHeight: .infinity)
      .background(Color.blue)
      .task {
        do {
          // ✅ task modifier로 image 로딩
          let imageData = try await ImageNetworkProvider.getImageFrom(url: url)
          self.imageData = imageData
        } catch {
          self.isErrorOccurred = true
          return
        }
      }
  }

  @ViewBuilder
  private func makeContent() -> some View {
    if let imageData{
      if let uiImage = UIImage(data: imageData) {
        Image(uiImage: uiImage)
          .resizable()
          .aspectRatio(contentMode: .fill)
      }else {
        makeErrorImageView()
          .onAppear{
            isErrorOccurred = true
          }
      }
    } else {
      if isErrorOccurred {
        makeErrorImageView()
      }
      else {
        ProgressView()
      }
    }
  }

  @ViewBuilder
  private func makeErrorImageView() -> some View {
    Image(systemName: "x.circle.fill")
      .resizable()
      .aspectRatio(contentMode: .fit)
  }
}

 

// MARK: - ImageNetworkProvider

enum ImageNetworkProvider {
  static func getImageFrom(url: URL) async throws -> Data {
    var urlRequest = URLRequest(url: url)
    urlRequest.cachePolicy = .returnCacheDataElseLoad
    let (data, _) = try await Constants.session.data(for: urlRequest)
    return data
  }
}

// MARK: - Constants

private enum Constants {
  static let diskPath: String = "IMAGEL_CACHE"
  static let cache: URLCache = {
    let memoryCapacity = 50 * 1024 * 1024
    let diskCapacity = 100 * 1024 * 1024
    return URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: diskPath)
  }()

  static let session: URLSession = {
    let config = URLSessionConfiguration.default
    config.urlCache = Constants.cache
    config.requestCachePolicy = .returnCacheDataElseLoad // 캐시가 있으면 사용, 없으면 네트워크 요청

    return URLSession(configuration: config, delegate: nil, delegateQueue: nil)
  }()
}

 

 

 

 

'Swift > swiftUI' 카테고리의 다른 글

[SwiftUI] TaskModifier  (0) 2024.05.06
[TCA] day 6 TCA의 SharedState에 대해서 (GettingStarted-SharedState)  (0) 2024.04.27