mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-23 05:19:42 +01:00
Widget: Add Hashtag widget
This commit is contained in:
parent
8ab7b5ac69
commit
ea31cda3c2
@ -71,7 +71,7 @@
|
|||||||
9F7788C92BE652B1004E6BEF /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7788C82BE652B1004E6BEF /* SwiftUI.framework */; };
|
9F7788C92BE652B1004E6BEF /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7788C82BE652B1004E6BEF /* SwiftUI.framework */; };
|
||||||
9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */; };
|
9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */; };
|
||||||
9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */; };
|
9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */; };
|
||||||
9F7788D02BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */; };
|
9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */; };
|
||||||
9F7788D22BE652B2004E6BEF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F7788D12BE652B2004E6BEF /* Assets.xcassets */; };
|
9F7788D22BE652B2004E6BEF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F7788D12BE652B2004E6BEF /* Assets.xcassets */; };
|
||||||
9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
9F7788DE2BE6543D004E6BEF /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DD2BE6543D004E6BEF /* Account */; };
|
9F7788DE2BE6543D004E6BEF /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DD2BE6543D004E6BEF /* Account */; };
|
||||||
@ -120,6 +120,10 @@
|
|||||||
9FE4CCAB2B4C848A00DA5F13 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = 9FE4CCAA2B4C848A00DA5F13 /* GiphyUISDK */; };
|
9FE4CCAB2B4C848A00DA5F13 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = 9FE4CCAA2B4C848A00DA5F13 /* GiphyUISDK */; };
|
||||||
9FE4CCAD2B4C849F00DA5F13 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE4CCAC2B4C849F00DA5F13 /* GiphyUISDK */; };
|
9FE4CCAD2B4C849F00DA5F13 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE4CCAC2B4C849F00DA5F13 /* GiphyUISDK */; };
|
||||||
9FE6A42E2BD043A90055D388 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE6A42D2BD043A90055D388 /* RevenueCat */; };
|
9FE6A42E2BD043A90055D388 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE6A42D2BD043A90055D388 /* RevenueCat */; };
|
||||||
|
9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */; };
|
||||||
|
9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */; };
|
||||||
|
9FF2FB672BE7F816001560CE /* LastestPostsUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB652BE7F805001560CE /* LastestPostsUI.swift */; };
|
||||||
|
9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB682BE7F842001560CE /* SharedUtils.swift */; };
|
||||||
9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; };
|
9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; };
|
||||||
9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; };
|
9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; };
|
||||||
9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; };
|
9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; };
|
||||||
@ -256,7 +260,7 @@
|
|||||||
9F7788C82BE652B1004E6BEF /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
9F7788C82BE652B1004E6BEF /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||||
9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesAppWidgetsExtensionBundle.swift; sourceTree = "<group>"; };
|
9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesAppWidgetsExtensionBundle.swift; sourceTree = "<group>"; };
|
||||||
9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestPostsWidget.swift; sourceTree = "<group>"; };
|
9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestPostsWidget.swift; sourceTree = "<group>"; };
|
||||||
9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesWidgetConfigurationIntent.swift; sourceTree = "<group>"; };
|
9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestPostsWidgetConfiguration.swift; sourceTree = "<group>"; };
|
||||||
9F7788D12BE652B2004E6BEF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
9F7788D12BE652B2004E6BEF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
9F7788D32BE652B2004E6BEF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
9F7788D32BE652B2004E6BEF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesAppWidgetsExtensionExtension.entitlements; sourceTree = "<group>"; };
|
9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesAppWidgetsExtensionExtension.entitlements; sourceTree = "<group>"; };
|
||||||
@ -290,6 +294,10 @@
|
|||||||
9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
|
9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
|
||||||
9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = "<group>"; };
|
9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = "<group>"; };
|
||||||
9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; };
|
9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; };
|
||||||
|
9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidgetConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidget.swift; sourceTree = "<group>"; };
|
||||||
|
9FF2FB652BE7F805001560CE /* LastestPostsUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastestPostsUI.swift; sourceTree = "<group>"; };
|
||||||
|
9FF2FB682BE7F842001560CE /* SharedUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUtils.swift; sourceTree = "<group>"; };
|
||||||
B0BAB49E29B3D7A9008F54D7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
B0BAB49E29B3D7A9008F54D7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
C4CBB90B298A0DA3007E1707 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
C4CBB90B298A0DA3007E1707 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
C4FBCF6F298FD88A0015DF22 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
C4FBCF6F298FD88A0015DF22 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
@ -471,10 +479,11 @@
|
|||||||
9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = {
|
9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
9FF2FB642BE7F7FA001560CE /* Shared */,
|
||||||
|
9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */,
|
||||||
|
9FF2FB5C2BE7F549001560CE /* LatestPosts */,
|
||||||
9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */,
|
9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */,
|
||||||
9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */,
|
9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */,
|
||||||
9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */,
|
|
||||||
9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */,
|
|
||||||
9F7788D12BE652B2004E6BEF /* Assets.xcassets */,
|
9F7788D12BE652B2004E6BEF /* Assets.xcassets */,
|
||||||
9F7788D32BE652B2004E6BEF /* Info.plist */,
|
9F7788D32BE652B2004E6BEF /* Info.plist */,
|
||||||
);
|
);
|
||||||
@ -632,6 +641,33 @@
|
|||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
9FF2FB5C2BE7F549001560CE /* LatestPosts */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */,
|
||||||
|
9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */,
|
||||||
|
);
|
||||||
|
path = LatestPosts;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */,
|
||||||
|
9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */,
|
||||||
|
);
|
||||||
|
path = HashtagPostsWidget;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9FF2FB642BE7F7FA001560CE /* Shared */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9FF2FB652BE7F805001560CE /* LastestPostsUI.swift */,
|
||||||
|
9FF2FB682BE7F842001560CE /* SharedUtils.swift */,
|
||||||
|
);
|
||||||
|
path = Shared;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
E9B576C029743F2A00BCE646 /* Localization */ = {
|
E9B576C029743F2A00BCE646 /* Localization */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -945,11 +981,15 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */,
|
||||||
9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */,
|
9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */,
|
||||||
|
9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */,
|
||||||
9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */,
|
9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */,
|
||||||
9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */,
|
9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */,
|
||||||
9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */,
|
9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */,
|
||||||
9F7788D02BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift in Sources */,
|
9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */,
|
||||||
|
9FF2FB672BE7F816001560CE /* LastestPostsUI.swift in Sources */,
|
||||||
|
9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
import Network
|
||||||
|
import DesignSystem
|
||||||
|
import Models
|
||||||
|
import Timeline
|
||||||
|
|
||||||
|
struct HashtagPostsWidgetProvider: AppIntentTimelineProvider {
|
||||||
|
func placeholder(in context: Context) -> LatestPostWidgetEntry {
|
||||||
|
.init(date: Date(),
|
||||||
|
timeline: .hashtag(tag: "Mastodon", accountId: nil),
|
||||||
|
statuses: [.placeholder()],
|
||||||
|
images: [:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> LatestPostWidgetEntry {
|
||||||
|
if let entry = await timeline(for: configuration, context: context).entries.first {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
return .init(date: Date(),
|
||||||
|
timeline: .hashtag(tag: "Mastodon", accountId: nil),
|
||||||
|
statuses: [],
|
||||||
|
images: [:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> Timeline<LatestPostWidgetEntry> {
|
||||||
|
await timeline(for: configuration, context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline<LatestPostWidgetEntry> {
|
||||||
|
guard let account = configuration.account, let hashgtag = configuration.hashgtag else {
|
||||||
|
return Timeline(entries: [.init(date: Date(),
|
||||||
|
timeline: .hashtag(tag: "Mastodon", accountId: nil),
|
||||||
|
statuses: [],
|
||||||
|
images: [:])],
|
||||||
|
policy: .atEnd)
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let statuses = await loadStatuses(for: .hashtag(tag: hashgtag, accountId: nil),
|
||||||
|
account: account,
|
||||||
|
widgetFamily: context.family)
|
||||||
|
let images = try await loadImages(urls: statuses.map{ $0.account.avatar } )
|
||||||
|
return Timeline(entries: [.init(date: Date(),
|
||||||
|
timeline: .hashtag(tag: hashgtag, accountId: nil),
|
||||||
|
statuses: statuses,
|
||||||
|
images: images)], policy: .atEnd)
|
||||||
|
} catch {
|
||||||
|
return Timeline(entries: [.init(date: Date(),
|
||||||
|
timeline: .hashtag(tag: "Mastodon", accountId: nil),
|
||||||
|
statuses: [],
|
||||||
|
images: [:])],
|
||||||
|
policy: .atEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HashtagPostsWidget: Widget {
|
||||||
|
let kind: String = "HashtagPostsWidget"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: HashtagPostsWidgetConfiguration.self,
|
||||||
|
provider: HashtagPostsWidgetProvider()) { entry in
|
||||||
|
LatestPostsWidgetView(entry: entry)
|
||||||
|
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Hashtag timeline")
|
||||||
|
.description("Show the latest post for the selected hashtag")
|
||||||
|
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#Preview(as: .systemMedium) {
|
||||||
|
HashtagPostsWidget()
|
||||||
|
} timeline: {
|
||||||
|
LatestPostWidgetEntry(date: .now,
|
||||||
|
timeline: .hashtag(tag: "Matodon", accountId: nil),
|
||||||
|
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
|
||||||
|
images: [:])
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import AppIntents
|
||||||
|
|
||||||
|
struct HashtagPostsWidgetConfiguration: WidgetConfigurationIntent {
|
||||||
|
static let title: LocalizedStringResource = "Configuration"
|
||||||
|
static let description = IntentDescription("Choose the account and hashtag for this widget")
|
||||||
|
|
||||||
|
@Parameter(title: "Account")
|
||||||
|
var account: AppAccountEntity?
|
||||||
|
|
||||||
|
@Parameter(title: "Hashtag")
|
||||||
|
var hashgtag: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HashtagPostsWidgetConfiguration {
|
||||||
|
static var previewAccount: HashtagPostsWidgetConfiguration {
|
||||||
|
let intent = HashtagPostsWidgetConfiguration()
|
||||||
|
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
|
||||||
|
intent.hashgtag = "Mastodon"
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}
|
@ -5,5 +5,6 @@ import SwiftUI
|
|||||||
struct IceCubesAppWidgetsExtensionBundle: WidgetBundle {
|
struct IceCubesAppWidgetsExtensionBundle: WidgetBundle {
|
||||||
var body: some Widget {
|
var body: some Widget {
|
||||||
LatestPostsWidget()
|
LatestPostsWidget()
|
||||||
|
HashtagPostsWidget()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
100
IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift
Normal file
100
IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
import Network
|
||||||
|
import DesignSystem
|
||||||
|
import Models
|
||||||
|
import Timeline
|
||||||
|
|
||||||
|
struct LatestPostsWidgetProvider: AppIntentTimelineProvider {
|
||||||
|
func placeholder(in context: Context) -> LatestPostWidgetEntry {
|
||||||
|
.init(date: Date(),
|
||||||
|
timeline: .home,
|
||||||
|
statuses: [.placeholder()],
|
||||||
|
images: [:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> LatestPostWidgetEntry {
|
||||||
|
if let entry = await timeline(for: configuration, context: context).entries.first {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
return .init(date: Date(),
|
||||||
|
timeline: .home, statuses: [],
|
||||||
|
images: [:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> Timeline<LatestPostWidgetEntry> {
|
||||||
|
await timeline(for: configuration, context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline<LatestPostWidgetEntry> {
|
||||||
|
guard let account = configuration.account, let timeline = configuration.timeline else {
|
||||||
|
return Timeline(entries: [.init(date: Date(),
|
||||||
|
timeline: .home,
|
||||||
|
statuses: [],
|
||||||
|
images: [:])],
|
||||||
|
policy: .atEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let statuses = await loadStatuses(for: timeline.timeline,
|
||||||
|
account: account,
|
||||||
|
widgetFamily: context.family)
|
||||||
|
let images = try await loadImages(urls: statuses.map{ $0.account.avatar } )
|
||||||
|
return Timeline(entries: [.init(date: Date(),
|
||||||
|
timeline: timeline.timeline,
|
||||||
|
statuses: statuses,
|
||||||
|
images: images)], policy: .atEnd)
|
||||||
|
} catch {
|
||||||
|
return Timeline(entries: [.init(date: Date(),
|
||||||
|
timeline: .home,
|
||||||
|
statuses: [],
|
||||||
|
images: [:])],
|
||||||
|
policy: .atEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadImages(urls: [URL]) async throws -> [URL: UIImage] {
|
||||||
|
try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in
|
||||||
|
for url in urls {
|
||||||
|
group.addTask {
|
||||||
|
let response = try await URLSession.shared.data(from: url)
|
||||||
|
return (url, UIImage(data: response.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var images: [URL: UIImage] = [:]
|
||||||
|
|
||||||
|
for try await (url, image) in group {
|
||||||
|
images[url] = image
|
||||||
|
}
|
||||||
|
|
||||||
|
return images
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LatestPostsWidget: Widget {
|
||||||
|
let kind: String = "LatestPostsWidget"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: LatestPostsWidgetConfiguration.self,
|
||||||
|
provider: LatestPostsWidgetProvider()) { entry in
|
||||||
|
LatestPostsWidgetView(entry: entry)
|
||||||
|
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Latest posts")
|
||||||
|
.description("Show the latest post for the selected timeline")
|
||||||
|
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#Preview(as: .systemMedium) {
|
||||||
|
LatestPostsWidget()
|
||||||
|
} timeline: {
|
||||||
|
LatestPostWidgetEntry(date: .now,
|
||||||
|
timeline: .home,
|
||||||
|
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
|
||||||
|
images: [:])
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import WidgetKit
|
import WidgetKit
|
||||||
import AppIntents
|
import AppIntents
|
||||||
|
|
||||||
struct IceCubesWidgetConfigurationIntent: WidgetConfigurationIntent {
|
struct LatestPostsWidgetConfiguration: WidgetConfigurationIntent {
|
||||||
static let title: LocalizedStringResource = "Configuration"
|
static let title: LocalizedStringResource = "Configuration"
|
||||||
static let description = IntentDescription("Choose the account and timeline for this widget")
|
static let description = IntentDescription("Choose the account and timeline for this widget")
|
||||||
|
|
||||||
@ -12,9 +12,9 @@ struct IceCubesWidgetConfigurationIntent: WidgetConfigurationIntent {
|
|||||||
var timeline: TimelineFilterEntity?
|
var timeline: TimelineFilterEntity?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension IceCubesWidgetConfigurationIntent {
|
extension LatestPostsWidgetConfiguration {
|
||||||
static var previewAccount: IceCubesWidgetConfigurationIntent {
|
static var previewAccount: LatestPostsWidgetConfiguration {
|
||||||
let intent = IceCubesWidgetConfigurationIntent()
|
let intent = LatestPostsWidgetConfiguration()
|
||||||
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
|
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
|
||||||
intent.timeline = .init(timeline: .home)
|
intent.timeline = .init(timeline: .home)
|
||||||
return intent
|
return intent
|
@ -1,210 +0,0 @@
|
|||||||
import WidgetKit
|
|
||||||
import SwiftUI
|
|
||||||
import Network
|
|
||||||
import DesignSystem
|
|
||||||
import Models
|
|
||||||
import Timeline
|
|
||||||
|
|
||||||
struct LatestPostsWidgetProvider: AppIntentTimelineProvider {
|
|
||||||
func placeholder(in context: Context) -> LatestPostWidgetEntry {
|
|
||||||
.init(date: Date(),
|
|
||||||
configuration: IceCubesWidgetConfigurationIntent(),
|
|
||||||
timeline: .home,
|
|
||||||
statuses: [.placeholder()],
|
|
||||||
images: [:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func snapshot(for configuration: IceCubesWidgetConfigurationIntent, in context: Context) async -> LatestPostWidgetEntry {
|
|
||||||
if let entry = await timeline(for: configuration, context: context).entries.first {
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
return .init(date: Date(),
|
|
||||||
configuration: configuration,
|
|
||||||
timeline: .home, statuses: [],
|
|
||||||
images: [:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func timeline(for configuration: IceCubesWidgetConfigurationIntent, in context: Context) async -> Timeline<LatestPostWidgetEntry> {
|
|
||||||
await timeline(for: configuration, context: context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func timeline(for configuration: IceCubesWidgetConfigurationIntent, context: Context) async -> Timeline<LatestPostWidgetEntry> {
|
|
||||||
guard let account = configuration.account, let timeline = configuration.timeline else {
|
|
||||||
return Timeline(entries: [.init(date: Date(),
|
|
||||||
configuration: configuration,
|
|
||||||
timeline: .home,
|
|
||||||
statuses: [],
|
|
||||||
images: [:])],
|
|
||||||
policy: .atEnd)
|
|
||||||
}
|
|
||||||
let client = Client(server: account.account.server, oauthToken: account.account.oauthToken)
|
|
||||||
do {
|
|
||||||
var statuses: [Status] = try await client.get(endpoint: timeline.timeline.endpoint(sinceId: nil,
|
|
||||||
maxId: nil,
|
|
||||||
minId: nil,
|
|
||||||
offset: nil))
|
|
||||||
statuses = statuses.filter{ $0.reblog == nil && !$0.content.asRawText.isEmpty }
|
|
||||||
switch context.family {
|
|
||||||
case .systemSmall, .systemMedium:
|
|
||||||
if statuses.count >= 1 {
|
|
||||||
statuses = statuses.prefix(upTo: 1).map{ $0 }
|
|
||||||
}
|
|
||||||
case .systemLarge, .systemExtraLarge:
|
|
||||||
if statuses.count >= 4 {
|
|
||||||
statuses = statuses.prefix(upTo: 4).map{ $0 }
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let images = try await loadImages(urls: statuses.map{ $0.account.avatar })
|
|
||||||
return Timeline(entries: [.init(date: Date(), configuration: configuration,
|
|
||||||
timeline: timeline.timeline,
|
|
||||||
statuses: statuses,
|
|
||||||
images: images)], policy: .atEnd)
|
|
||||||
} catch {
|
|
||||||
return Timeline(entries: [.init(date: Date(),
|
|
||||||
configuration: configuration,
|
|
||||||
timeline: .home,
|
|
||||||
statuses: [],
|
|
||||||
images: [:])], policy: .atEnd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadImages(urls: [URL]) async throws -> [URL: UIImage] {
|
|
||||||
try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in
|
|
||||||
for url in urls {
|
|
||||||
group.addTask {
|
|
||||||
let response = try await URLSession.shared.data(from: url)
|
|
||||||
return (url, UIImage(data: response.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var images: [URL: UIImage] = [:]
|
|
||||||
|
|
||||||
for try await (url, image) in group {
|
|
||||||
images[url] = image
|
|
||||||
}
|
|
||||||
|
|
||||||
return images
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LatestPostWidgetEntry: TimelineEntry {
|
|
||||||
let date: Date
|
|
||||||
let configuration: IceCubesWidgetConfigurationIntent
|
|
||||||
let timeline: TimelineFilter
|
|
||||||
let statuses: [Status]
|
|
||||||
let images: [URL: UIImage]
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LatestPostsWidgetView : View {
|
|
||||||
var entry: LatestPostsWidgetProvider.Entry
|
|
||||||
|
|
||||||
@Environment(\.widgetFamily) var family
|
|
||||||
@Environment(\.redactionReasons) var redacted
|
|
||||||
|
|
||||||
var contentLineLimit: Int {
|
|
||||||
switch family {
|
|
||||||
case .systemSmall, .systemMedium:
|
|
||||||
return 4
|
|
||||||
default:
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
headerView
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
ForEach(entry.statuses) { status in
|
|
||||||
makeStatusView(status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var headerView: some View {
|
|
||||||
HStack {
|
|
||||||
Text(entry.timeline.title)
|
|
||||||
Spacer()
|
|
||||||
Image(systemName: "cube")
|
|
||||||
}
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundStyle(Color("AccentColor"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func makeStatusView(_ status: Status) -> some View {
|
|
||||||
if let url = URL(string: status.url ?? "") {
|
|
||||||
Link(destination: url, label: {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
makeStatusHeaderView(status)
|
|
||||||
Text(status.content.asSafeMarkdownAttributedString)
|
|
||||||
.font(.body)
|
|
||||||
.lineLimit(contentLineLimit)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.padding(.leading, 20)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeStatusHeaderView(_ status: Status) -> some View {
|
|
||||||
HStack(alignment: .center, spacing: 4) {
|
|
||||||
if let image = entry.images[status.account.avatar] {
|
|
||||||
Image(uiImage: image)
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 16, height: 16)
|
|
||||||
.clipShape(Circle())
|
|
||||||
} else {
|
|
||||||
Circle()
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.frame(width: 16, height: 16)
|
|
||||||
}
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Text(status.account.safeDisplayName)
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
if family != .systemSmall {
|
|
||||||
Text(" @")
|
|
||||||
.foregroundStyle(.tertiary)
|
|
||||||
Text(status.account.username)
|
|
||||||
.foregroundStyle(.tertiary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LatestPostsWidget: Widget {
|
|
||||||
let kind: String = "LatestPostsWidget"
|
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
|
||||||
AppIntentConfiguration(kind: kind,
|
|
||||||
intent: IceCubesWidgetConfigurationIntent.self,
|
|
||||||
provider: LatestPostsWidgetProvider()) { entry in
|
|
||||||
LatestPostsWidgetView(entry: entry)
|
|
||||||
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
|
|
||||||
}
|
|
||||||
.configurationDisplayName("Latest posts")
|
|
||||||
.description("Show the latest post for the selected timeline")
|
|
||||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#Preview(as: .systemMedium) {
|
|
||||||
LatestPostsWidget()
|
|
||||||
} timeline: {
|
|
||||||
LatestPostWidgetEntry(date: .now,
|
|
||||||
configuration: .previewAccount,
|
|
||||||
timeline: .home,
|
|
||||||
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
|
|
||||||
images: [:])
|
|
||||||
}
|
|
97
IceCubesAppWidgetsExtension/Shared/LastestPostsUI.swift
Normal file
97
IceCubesAppWidgetsExtension/Shared/LastestPostsUI.swift
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
import Network
|
||||||
|
import DesignSystem
|
||||||
|
import Models
|
||||||
|
import Timeline
|
||||||
|
|
||||||
|
struct LatestPostWidgetEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let timeline: TimelineFilter
|
||||||
|
let statuses: [Status]
|
||||||
|
let images: [URL: UIImage]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LatestPostsWidgetView : View {
|
||||||
|
var entry: LatestPostsWidgetProvider.Entry
|
||||||
|
|
||||||
|
@Environment(\.widgetFamily) var family
|
||||||
|
@Environment(\.redactionReasons) var redacted
|
||||||
|
|
||||||
|
var contentLineLimit: Int {
|
||||||
|
switch family {
|
||||||
|
case .systemSmall, .systemMedium:
|
||||||
|
return 4
|
||||||
|
default:
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
headerView
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
ForEach(entry.statuses) { status in
|
||||||
|
makeStatusView(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var headerView: some View {
|
||||||
|
HStack {
|
||||||
|
Text(entry.timeline.title)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "cube")
|
||||||
|
}
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundStyle(Color("AccentColor"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func makeStatusView(_ status: Status) -> some View {
|
||||||
|
if let url = URL(string: status.url ?? "") {
|
||||||
|
Link(destination: url, label: {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
makeStatusHeaderView(status)
|
||||||
|
Text(status.content.asSafeMarkdownAttributedString)
|
||||||
|
.font(.body)
|
||||||
|
.lineLimit(contentLineLimit)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.padding(.leading, 20)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeStatusHeaderView(_ status: Status) -> some View {
|
||||||
|
HStack(alignment: .center, spacing: 4) {
|
||||||
|
if let image = entry.images[status.account.avatar] {
|
||||||
|
Image(uiImage: image)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
|
.clipShape(Circle())
|
||||||
|
} else {
|
||||||
|
Circle()
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
|
}
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text(status.account.safeDisplayName)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
if family != .systemSmall {
|
||||||
|
Text(" @")
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
Text(status.account.username)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
IceCubesAppWidgetsExtension/Shared/SharedUtils.swift
Normal file
55
IceCubesAppWidgetsExtension/Shared/SharedUtils.swift
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import StatusKit
|
||||||
|
import WidgetKit
|
||||||
|
import Timeline
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AppAccount
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
|
||||||
|
func loadStatuses(for timeline: TimelineFilter,
|
||||||
|
account: AppAccountEntity,
|
||||||
|
widgetFamily: WidgetFamily) async -> [Status] {
|
||||||
|
let client = Client(server: account.account.server, oauthToken: account.account.oauthToken)
|
||||||
|
do {
|
||||||
|
var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||||
|
maxId: nil,
|
||||||
|
minId: nil,
|
||||||
|
offset: nil))
|
||||||
|
statuses = statuses.filter{ $0.reblog == nil && !$0.content.asRawText.isEmpty }
|
||||||
|
switch widgetFamily {
|
||||||
|
case .systemSmall, .systemMedium:
|
||||||
|
if statuses.count >= 1 {
|
||||||
|
statuses = statuses.prefix(upTo: 1).map{ $0 }
|
||||||
|
}
|
||||||
|
case .systemLarge, .systemExtraLarge:
|
||||||
|
if statuses.count >= 4 {
|
||||||
|
statuses = statuses.prefix(upTo: 4).map{ $0 }
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return statuses
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadImages(urls: [URL]) async throws -> [URL: UIImage] {
|
||||||
|
try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in
|
||||||
|
for url in urls {
|
||||||
|
group.addTask {
|
||||||
|
let response = try await URLSession.shared.data(from: url)
|
||||||
|
return (url, UIImage(data: response.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var images: [URL: UIImage] = [:]
|
||||||
|
|
||||||
|
for try await (url, image) in group {
|
||||||
|
images[url] = image
|
||||||
|
}
|
||||||
|
|
||||||
|
return images
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user