From 627c3541238282d4229f4f85c1110caa3e0b7d2f Mon Sep 17 00:00:00 2001 From: mltokky Date: Sat, 17 Aug 2024 01:38:24 +0900 Subject: [PATCH 1/7] Impl to display speaker icon using cache if speaker icon already fetched created View for speaker icon. --- .../Timetable/SpeakerIcon.swift | 42 +++++++++++++++++++ .../Timetable/TimetableCard.swift | 13 +----- 2 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift diff --git a/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift b/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift new file mode 100644 index 000000000..c09059601 --- /dev/null +++ b/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift @@ -0,0 +1,42 @@ +import SwiftUI + +struct SpeakerIcon: View { + let urlString: String + @State private var iconData: Data? + + var body: some View { + Group { + if let data = iconData, + let uiImage = UIImage(data: data) { + Image(uiImage: uiImage) + .resizable() + } else { + Circle().stroke(Color.gray) + } + } + .frame(width: 32, height: 32) + .clipShape(Circle()) + .task { + guard let url = URL(string: urlString) else { + return + } + let urlRequest = URLRequest(url: url) + if let cachedResponse = URLCache.shared.cachedResponse(for: urlRequest) { + iconData = cachedResponse.data + return + } + + do { + let (data, response) = try await URLSession.shared.data(for: urlRequest) + URLCache.shared.storeCachedResponse(CachedURLResponse(response: response, data: data), for: urlRequest) + iconData = data + } catch { + iconData = nil + } + } + } +} + +#Preview { + SpeakerIcon(urlString: "https://github.com/mltokky.png") +} diff --git a/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift b/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift index a89cb95ec..c58207d86 100644 --- a/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift +++ b/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift @@ -54,18 +54,7 @@ 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()) - + SpeakerIcon(urlString: speaker.iconUrl) Text(speaker.name) .textStyle(.titleSmall) .foregroundStyle(AssetColors.Surface.onSurfaceVariant.swiftUIColor) From eeb848e237b78cf7d755d2bdc2d6b61d9b276262 Mon Sep 17 00:00:00 2001 From: mltokky Date: Sat, 17 Aug 2024 22:17:57 +0900 Subject: [PATCH 2/7] impl in-memory cache for speaker icon --- .../Timetable/SpeakerIcon.swift | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift b/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift index c09059601..e23205481 100644 --- a/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift +++ b/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift @@ -1,9 +1,29 @@ +import Foundation import SwiftUI +private actor SpeakerIconInMemoryCache { + static let shared = SpeakerIconInMemoryCache() + private init() {} + + private var cache: [String: Data] = [:] + + func data(urlString: String) -> Data? { + return cache[urlString] + } + + func set(data: Data, urlString: String) { + cache[urlString] = data + } +} + struct SpeakerIcon: View { let urlString: String @State private var iconData: Data? + init(urlString: String) { + self.urlString = urlString + } + var body: some View { Group { if let data = iconData, @@ -17,21 +37,18 @@ struct SpeakerIcon: View { .frame(width: 32, height: 32) .clipShape(Circle()) .task { - guard let url = URL(string: urlString) else { + if let data = await SpeakerIconInMemoryCache.shared.data(urlString: urlString) { + iconData = data return } - let urlRequest = URLRequest(url: url) - if let cachedResponse = URLCache.shared.cachedResponse(for: urlRequest) { - iconData = cachedResponse.data + + guard let url = URL(string: urlString) else { return } - - do { - let (data, response) = try await URLSession.shared.data(for: urlRequest) - URLCache.shared.storeCachedResponse(CachedURLResponse(response: response, data: data), for: urlRequest) + let urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy) + if let (data, _) = try? await URLSession.shared.data(for: urlRequest) { iconData = data - } catch { - iconData = nil + await SpeakerIconInMemoryCache.shared.set(data: data, urlString: urlString) } } } From 59105f70fa89554c7eaa806a7ac0f2441caea038 Mon Sep 17 00:00:00 2001 From: mltokky Date: Tue, 20 Aug 2024 00:11:16 +0900 Subject: [PATCH 3/7] remove `frame()` in `SpeakerIcon` because It is for general purpose use. --- .../Sources/CommonComponents/Timetable/SpeakerIcon.swift | 8 ++++---- .../CommonComponents/Timetable/TimetableCard.swift | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift b/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift index e23205481..b2e0a56f0 100644 --- a/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift +++ b/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift @@ -16,15 +16,15 @@ private actor SpeakerIconInMemoryCache { } } -struct SpeakerIcon: View { +public struct SpeakerIcon: View { let urlString: String @State private var iconData: Data? - init(urlString: String) { + public init(urlString: String) { self.urlString = urlString } - var body: some View { + public var body: some View { Group { if let data = iconData, let uiImage = UIImage(data: data) { @@ -34,7 +34,6 @@ struct SpeakerIcon: View { Circle().stroke(Color.gray) } } - .frame(width: 32, height: 32) .clipShape(Circle()) .task { if let data = await SpeakerIconInMemoryCache.shared.data(urlString: urlString) { @@ -56,4 +55,5 @@ struct SpeakerIcon: View { #Preview { SpeakerIcon(urlString: "https://github.com/mltokky.png") + .frame(width: 32, height: 32) } diff --git a/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift b/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift index b4ee56eb5..1ffe87c0b 100644 --- a/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift +++ b/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift @@ -55,6 +55,7 @@ public struct TimetableCard: View { ForEach(timetableItem.speakers, id: \.id) { speaker in HStack(spacing: 8) { SpeakerIcon(urlString: speaker.iconUrl) + .frame(width: 32, height: 32) Text(speaker.name) .textStyle(.titleSmall) .foregroundStyle(AssetColors.Surface.onSurfaceVariant.swiftUIColor) From 19be4c08c0213a2f85e1b39370481eeaa4e72e20 Mon Sep 17 00:00:00 2001 From: mltokky Date: Tue, 20 Aug 2024 00:39:49 +0900 Subject: [PATCH 4/7] change to SpeakerIcon from AsyncImage of circular user icon. --- .../ContributorListItemView.swift | 8 +++----- app-ios/Sources/StaffFeature/StaffLabel.swift | 18 ++++++++---------- .../TimetableDetailView.swift | 7 +------ 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/app-ios/Sources/ContributorFeature/ContributorListItemView.swift b/app-ios/Sources/ContributorFeature/ContributorListItemView.swift index 3adc985ef..8c1a0cd0b 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()) + SpeakerIcon(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..819c5fefd 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) - ) + SpeakerIcon(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..f02f7052c 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() - } + SpeakerIcon(urlString: speaker.iconUrl) .frame(width: 52, height: 52) - .clipShape(Circle()) - } VStack(alignment: .leading, spacing: 8) { Text(speaker.name) From e611961e9840e5d6a26af70e64811ad0e6e22167 Mon Sep 17 00:00:00 2001 From: mltokky Date: Tue, 20 Aug 2024 00:50:33 +0900 Subject: [PATCH 5/7] rename to CircularUserIcon from SpeakerIcon --- .../Timetable/{SpeakerIcon.swift => CircularUserIcon.swift} | 4 ++-- .../Sources/CommonComponents/Timetable/TimetableCard.swift | 2 +- .../Sources/ContributorFeature/ContributorListItemView.swift | 2 +- app-ios/Sources/StaffFeature/StaffLabel.swift | 2 +- .../Sources/TimetableDetailFeature/TimetableDetailView.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename app-ios/Sources/CommonComponents/Timetable/{SpeakerIcon.swift => CircularUserIcon.swift} (93%) diff --git a/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift b/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift similarity index 93% rename from app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift rename to app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift index b2e0a56f0..a200c6b1f 100644 --- a/app-ios/Sources/CommonComponents/Timetable/SpeakerIcon.swift +++ b/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift @@ -16,7 +16,7 @@ private actor SpeakerIconInMemoryCache { } } -public struct SpeakerIcon: View { +public struct CircularUserIcon: View { let urlString: String @State private var iconData: Data? @@ -54,6 +54,6 @@ public struct SpeakerIcon: View { } #Preview { - SpeakerIcon(urlString: "https://github.com/mltokky.png") + CircularUserIcon(urlString: "https://github.com/mltokky.png") .frame(width: 32, height: 32) } diff --git a/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift b/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift index 1ffe87c0b..b82032b5f 100644 --- a/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift +++ b/app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift @@ -54,7 +54,7 @@ public struct TimetableCard: View { ForEach(timetableItem.speakers, id: \.id) { speaker in HStack(spacing: 8) { - SpeakerIcon(urlString: speaker.iconUrl) + CircularUserIcon(urlString: speaker.iconUrl) .frame(width: 32, height: 32) Text(speaker.name) .textStyle(.titleSmall) diff --git a/app-ios/Sources/ContributorFeature/ContributorListItemView.swift b/app-ios/Sources/ContributorFeature/ContributorListItemView.swift index 8c1a0cd0b..9c77c31bc 100644 --- a/app-ios/Sources/ContributorFeature/ContributorListItemView.swift +++ b/app-ios/Sources/ContributorFeature/ContributorListItemView.swift @@ -14,7 +14,7 @@ struct ContributorListItemView: View { } } label: { HStack(alignment: .center, spacing: 12) { - SpeakerIcon(urlString: contributor.iconUrl.absoluteString) + CircularUserIcon(urlString: contributor.iconUrl.absoluteString) .frame(width: 52, height: 52) Text(contributor.userName) diff --git a/app-ios/Sources/StaffFeature/StaffLabel.swift b/app-ios/Sources/StaffFeature/StaffLabel.swift index 819c5fefd..564bd7f14 100644 --- a/app-ios/Sources/StaffFeature/StaffLabel.swift +++ b/app-ios/Sources/StaffFeature/StaffLabel.swift @@ -8,7 +8,7 @@ struct StaffLabel: View { var body: some View { HStack(alignment: .center, spacing: 12) { - SpeakerIcon(urlString: icon.absoluteString) + CircularUserIcon(urlString: icon.absoluteString) .frame(width: 52, height: 52) .overlay( Circle() diff --git a/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift b/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift index f02f7052c..dd2afbc5d 100644 --- a/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift +++ b/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift @@ -109,7 +109,7 @@ public struct TimetableDetailView: View { ForEach(store.timetableItem.speakers, id: \.id) { speaker in HStack(spacing: 12) { - SpeakerIcon(urlString: speaker.iconUrl) + CircularUserIcon(urlString: speaker.iconUrl) .frame(width: 52, height: 52) VStack(alignment: .leading, spacing: 8) { From 167575060d1aa4e6b35d7dde92dc26eb9df9739c Mon Sep 17 00:00:00 2001 From: mltokky Date: Tue, 20 Aug 2024 12:55:03 +0900 Subject: [PATCH 6/7] change preview image of CircularUserIcon --- .../Sources/CommonComponents/Timetable/CircularUserIcon.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift b/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift index a200c6b1f..e4cfc1cfd 100644 --- a/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift +++ b/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift @@ -54,6 +54,6 @@ public struct CircularUserIcon: View { } #Preview { - CircularUserIcon(urlString: "https://github.com/mltokky.png") + CircularUserIcon(urlString: "https://avatars.githubusercontent.com/u/10727543?s=96&v=4") .frame(width: 32, height: 32) } From feddd2ee458515fef9d07d1dac03e2b1eac46171 Mon Sep 17 00:00:00 2001 From: mltokky Date: Tue, 20 Aug 2024 23:19:41 +0900 Subject: [PATCH 7/7] rename actor `SpeakerIconInMemoryCache` to `CircularUserIconInMemoryCache` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because related view is renamed, so I changed it accordingly. --- .../CommonComponents/Timetable/CircularUserIcon.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift b/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift index e4cfc1cfd..3101378f3 100644 --- a/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift +++ b/app-ios/Sources/CommonComponents/Timetable/CircularUserIcon.swift @@ -1,8 +1,8 @@ import Foundation import SwiftUI -private actor SpeakerIconInMemoryCache { - static let shared = SpeakerIconInMemoryCache() +private actor CircularUserIconInMemoryCache { + static let shared = CircularUserIconInMemoryCache() private init() {} private var cache: [String: Data] = [:] @@ -36,7 +36,7 @@ public struct CircularUserIcon: View { } .clipShape(Circle()) .task { - if let data = await SpeakerIconInMemoryCache.shared.data(urlString: urlString) { + if let data = await CircularUserIconInMemoryCache.shared.data(urlString: urlString) { iconData = data return } @@ -47,7 +47,7 @@ public struct CircularUserIcon: View { let urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy) if let (data, _) = try? await URLSession.shared.data(for: urlRequest) { iconData = data - await SpeakerIconInMemoryCache.shared.set(data: data, urlString: urlString) + await CircularUserIconInMemoryCache.shared.set(data: data, urlString: urlString) } } }