이번에 SwiftUI + ScrollView + ImageView 쓰면서 겪었던 트러블 슈팅을 정리해 보려고 합니다.
LazyLayout + AsyncImage
LazyLoyout과 AsynImage를 쓰면서 문제가 있었습니다. LazyLayout에 할당된 AsyncImage가 이상하게 동작했습니다. 우리는 LazyLayout을 통해 사용자 화면에 나타난 AsyncImage를 부를 것 입니다. 하지만 AsyncImage를 loading하다가 에러가 생기거나 연결이 끊긴 경우에 자동으로 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)
}
}
그래서 Custom한 AsyncImageView를 만들어서 활용했습니다. 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 |