diff --git a/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift b/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift new file mode 100644 index 000000000..3101378f3 --- /dev/null +++ b/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift @@ -0,0 +1,59 @@ +import Foundation +import SwiftUI + +private actor CircularUserIconInMemoryCache { + static let shared = CircularUserIconInMemoryCache() + private init() {} + + private var cache: [String: Data] = [:] + + func data(urlString: String) -> Data? { + return cache[urlString] + } + + func set(data: Data, urlString: String) { + cache[urlString] = data + } +} + +public struct CircularUserIcon: View { + let urlString: String + @State private var iconData: Data? + + public init(urlString: String) { + self.urlString = urlString + } + + public var body: some View { + Group { + if let data = iconData, + let uiImage = UIImage(data: data) { + Image(uiImage: uiImage) + .resizable() + } else { + Circle().stroke(Color.gray) + } + } + .clipShape(Circle()) + .task { + if let data = await CircularUserIconInMemoryCache.shared.data(urlString: urlString) { + iconData = data + return + } + + guard let url = URL(string: urlString) else { + return + } + let urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy) + if let (data, _) = try? await URLSession.shared.data(for: urlRequest) { + iconData = data + await CircularUserIconInMemoryCache.shared.set(data: data, urlString: urlString) + } + } + } +} + +#Preview { + CircularUserIcon(urlString: "https://avatars.githubusercontent.com/u/10727543?s=96&v=4") + .frame(width: 32, height: 32) +} diff --git a/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift b/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift index 3afe811a2..b82032b5f 100644 --- a/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift +++ b/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift @@ -54,18 +54,8 @@ public struct TimetableCard: View { ForEach(timetableItem.speakers, id: \.id) { speaker in HStack(spacing: 8) { - Group { - if let url = URL(string: speaker.iconUrl) { - AsyncImage(url: url) { - $0.image?.resizable() - } - } else { - Circle().stroke(Color.gray) - } - } - .frame(width: 32, height: 32) - .clipShape(Circle()) - + CircularUserIcon(urlString: speaker.iconUrl) + .frame(width: 32, height: 32) Text(speaker.name) .textStyle(.titleSmall) .foregroundStyle(AssetColors.Surface.onSurfaceVariant.swiftUIColor) diff --git a/app-ios/Sources/ContributorFeature/ContributorListItemView.swift b/app-ios/Sources/ContributorFeature/ContributorListItemView.swift index 3adc985ef..9c77c31bc 100644 --- a/app-ios/Sources/ContributorFeature/ContributorListItemView.swift +++ b/app-ios/Sources/ContributorFeature/ContributorListItemView.swift @@ -1,6 +1,7 @@ import SwiftUI import Theme import Model +import CommonComponents struct ContributorListItemView: View { let contributor: Contributor @@ -13,11 +14,8 @@ struct ContributorListItemView: View { } } label: { HStack(alignment: .center, spacing: 12) { - AsyncImage(url: contributor.iconUrl) { - $0.image?.resizable() - } - .frame(width: 52, height: 52) - .clipShape(Circle()) + CircularUserIcon(urlString: contributor.iconUrl.absoluteString) + .frame(width: 52, height: 52) Text(contributor.userName) .textStyle(.bodyLarge) diff --git a/app-ios/Sources/StaffFeature/StaffLabel.swift b/app-ios/Sources/StaffFeature/StaffLabel.swift index 88d9a0814..564bd7f14 100644 --- a/app-ios/Sources/StaffFeature/StaffLabel.swift +++ b/app-ios/Sources/StaffFeature/StaffLabel.swift @@ -1,5 +1,6 @@ import SwiftUI import Theme +import CommonComponents struct StaffLabel: View { let name: String @@ -7,15 +8,12 @@ struct StaffLabel: View { var body: some View { HStack(alignment: .center, spacing: 12) { - AsyncImage(url: icon) { - $0.image?.resizable() - } - .frame(width: 52, height: 52) - .clipShape(Circle()) - .overlay( - Circle() - .stroke(AssetColors.Outline.outline.swiftUIColor, lineWidth: 1) - ) + CircularUserIcon(urlString: icon.absoluteString) + .frame(width: 52, height: 52) + .overlay( + Circle() + .stroke(AssetColors.Outline.outline.swiftUIColor, lineWidth: 1) + ) Text(name) .textStyle(.bodyLarge) @@ -27,5 +25,5 @@ struct StaffLabel: View { } #Preview { - StaffLabel(name: "hoge", icon: .init(string: "")!) + StaffLabel(name: "hoge", icon: .init(string: "https://avatars.githubusercontent.com/u/10727543?s=156&v=4")!) } diff --git a/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift b/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift index c3704045b..dd2afbc5d 100644 --- a/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift +++ b/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift @@ -109,13 +109,8 @@ public struct TimetableDetailView: View { ForEach(store.timetableItem.speakers, id: \.id) { speaker in HStack(spacing: 12) { - if let url = URL(string: speaker.iconUrl) { - AsyncImage(url: url) { - $0.image?.resizable() - } + CircularUserIcon(urlString: speaker.iconUrl) .frame(width: 52, height: 52) - .clipShape(Circle()) - } VStack(alignment: .leading, spacing: 8) { Text(speaker.name)