diff --git a/SodaLive/Sources/Chat/Character/Banner/AutoSlideCharacterBannerView.swift b/SodaLive/Sources/Chat/Character/Banner/AutoSlideCharacterBannerView.swift index 6d61f6f..ad04973 100644 --- a/SodaLive/Sources/Chat/Character/Banner/AutoSlideCharacterBannerView.swift +++ b/SodaLive/Sources/Chat/Character/Banner/AutoSlideCharacterBannerView.swift @@ -84,16 +84,10 @@ private struct AutoSlideCharacterBannerPage: View { var body: some View { Group { if let boundURL { - KFImage(boundURL) - .placeholder { Color.gray.opacity(0.2) } - .retry(maxCount: 2, interval: .seconds(1)) - .cancelOnDisappear(true) - .downsampling(size: CGSize(width: width, height: height)) - .resizable() - .scaledToFill() - .frame(width: width, height: height) - .clipped() - .cornerRadius(12) + DownsampledKFImage( + url: boundURL, + size: CGSize(width: width, height: height) + ).cornerRadius(12) } else { Color.clear .frame(width: width, height: height) diff --git a/SodaLive/Sources/Chat/Character/CharacterItemView.swift b/SodaLive/Sources/Chat/Character/CharacterItemView.swift index 7716c36..0d192b2 100644 --- a/SodaLive/Sources/Chat/Character/CharacterItemView.swift +++ b/SodaLive/Sources/Chat/Character/CharacterItemView.swift @@ -22,15 +22,11 @@ struct CharacterItemView: View { var body: some View { VStack(alignment: .leading, spacing: 4) { ZStack(alignment: .bottomLeading) { - KFImage(URL(string: character.imageUrl)) - .placeholder { Color.gray.opacity(0.2) } - .retry(maxCount: 2, interval: .seconds(1)) - .cancelOnDisappear(true) - .resizable() - .scaledToFill() - .frame(width: size, height: size) - .clipped() - .cornerRadius(12) + DownsampledKFImage( + url: URL(string: character.imageUrl), + size: CGSize(width: size, height: size) + ) + .cornerRadius(12) if isShowRank { Text("\(rank)") diff --git a/SodaLive/Sources/Chat/Character/CharacterSectionView.swift b/SodaLive/Sources/Chat/Character/CharacterSectionView.swift index 077153d..d758ee6 100644 --- a/SodaLive/Sources/Chat/Character/CharacterSectionView.swift +++ b/SodaLive/Sources/Chat/Character/CharacterSectionView.swift @@ -32,7 +32,7 @@ struct CharacterSectionView: View { .padding(.horizontal, 24) ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 16) { + LazyHStack(spacing: 16) { ForEach(items.indices, id: \.self) { idx in let item = items[idx] CharacterItemView( diff --git a/SodaLive/Sources/Chat/Character/Recent/RecentCharacterItemView.swift b/SodaLive/Sources/Chat/Character/Recent/RecentCharacterItemView.swift index c6454f1..e553c81 100644 --- a/SodaLive/Sources/Chat/Character/Recent/RecentCharacterItemView.swift +++ b/SodaLive/Sources/Chat/Character/Recent/RecentCharacterItemView.swift @@ -13,14 +13,11 @@ struct RecentCharacterItemView: View { var body: some View { VStack(spacing: 6) { - KFImage(URL(string: character.imageUrl)) - .placeholder { Circle().fill(Color.gray.opacity(0.2)) } - .retry(maxCount: 2, interval: .seconds(1)) - .cancelOnDisappear(true) - .resizable() - .scaledToFill() - .frame(width: 76, height: 76) - .clipShape(Circle()) + DownsampledKFImage( + url: URL(string: character.imageUrl), + size: CGSize(width: 76, height: 76) + ) + .clipShape(Circle()) Text(character.name) .font(.custom(Font.preRegular.rawValue, size: 18)) diff --git a/SodaLive/Sources/Chat/Character/Recent/RecentCharacterSectionView.swift b/SodaLive/Sources/Chat/Character/Recent/RecentCharacterSectionView.swift index c493af2..b3eb30a 100644 --- a/SodaLive/Sources/Chat/Character/Recent/RecentCharacterSectionView.swift +++ b/SodaLive/Sources/Chat/Character/Recent/RecentCharacterSectionView.swift @@ -28,7 +28,7 @@ struct RecentCharacterSectionView: View { .padding(.horizontal, 24) ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 16) { + LazyHStack(spacing: 16) { ForEach(items.indices, id: \.self) { idx in let item = items[idx] RecentCharacterItemView(character: item) diff --git a/SodaLive/Sources/CustomView/DownsampledKFImage.swift b/SodaLive/Sources/CustomView/DownsampledKFImage.swift new file mode 100644 index 0000000..3767780 --- /dev/null +++ b/SodaLive/Sources/CustomView/DownsampledKFImage.swift @@ -0,0 +1,45 @@ +// +// DownsampledKFImage.swift +// SodaLive +// +// Created by klaus on 10/23/25. +// + +import SwiftUI +import Kingfisher + +struct DownsampledKFImage: View { + let url: URL? + let size: CGSize + let cacheOriginal: Bool = false + + var body: some View { + KFImage(url) + .placeholder { Color.gray.opacity(0.2) } + .retry(maxCount: 2, interval: .seconds(1)) + .cancelOnDisappear(true) + .downsampled(to: size, cacheOriginal: cacheOriginal) + .resizable() + .scaledToFill() + .frame(width: size.width, height: size.height) + .clipped() + } +} + +extension KFImage { + func downsampled( + to targetSize: CGSize, + scale: CGFloat = UIScreen.main.scale, + cacheOriginal: Bool = false + ) -> KFImage { + let pixel = CGSize( + width: targetSize.width * scale, + height: targetSize.height * scale + ) + return self + .setProcessor(DownsamplingImageProcessor(size: pixel)) + .scaleFactor(scale) + .backgroundDecode() + .cacheOriginalImage(cacheOriginal) + } +}