From ba825d4218071b8ef468b84331dc50e3b5ee9674 Mon Sep 17 00:00:00 2001 From: Magesh K <47920326+mahee96@users.noreply.github.com> Date: Sat, 11 Jan 2025 21:31:10 +0530 Subject: [PATCH] [Widgets]: cleanup: refactored to use guard-else flow instead of if-else flow --- AltWidget/Intents/PaginationIntent.swift | 22 +- AltWidget/Intents/WidgetUpdateIntent.swift | 1 + AltWidget/Manager/PageInfoManager.swift | 22 +- .../ActiveAppsTimelineProvider.swift | 83 +++++-- AltWidget/Widgets/ActiveAppsWidget.swift | 218 ++++++++---------- .../pagination/PaginationDataHolder.swift | 5 + 6 files changed, 198 insertions(+), 153 deletions(-) diff --git a/AltWidget/Intents/PaginationIntent.swift b/AltWidget/Intents/PaginationIntent.swift index 220981e4..f68df42c 100644 --- a/AltWidget/Intents/PaginationIntent.swift +++ b/AltWidget/Intents/PaginationIntent.swift @@ -18,13 +18,12 @@ public enum Direction: String, Sendable{ public struct NavigationEvent { let direction: Direction? var consumed: Bool = false + var dataHolder: PaginationDataHolder? } @available(iOS 17, *) class PaginationIntent: AppIntent, @unchecked Sendable { - - private let COMMON_WIDGET_ID = 1 - + static var title: LocalizedStringResource = "Page Navigation Intent" static var isDiscoverable: Bool = false @@ -45,17 +44,28 @@ class PaginationIntent: AppIntent, @unchecked Sendable { // if id was not passed in, then we assume the widget isn't customized yet // hence we use the common ID, if this is not present in registry of PageInfoManager // then it will return nil, triggering to show first page in the provider - self.widgetID = widgetID ?? COMMON_WIDGET_ID + self.widgetID = widgetID ?? WidgetUpdateIntent.COMMON_WIDGET_ID self.direction = direction.rawValue self.widgetKind = widgetKind } func perform() async throws -> some IntentResult { + // Post the navigation event into the shared db (Dictionary) and ask to reload DispatchQueue(label: String(widgetID)).sync { - let navigationEvent = NavigationEvent(direction: Direction(rawValue: direction)) - PageInfoManager.shared.setPageInfo(for: widgetID, value: navigationEvent) + self.postThisNavigationEvent() WidgetCenter.shared.reloadTimelines(ofKind: widgetKind) } return .result() } + + private func postThisNavigationEvent(){ + // re-use an existing event if available and update only required parts + let navEvent = PageInfoManager.shared.getPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID) + let navigationEvent = NavigationEvent( + direction: Direction(rawValue: direction), + consumed: false, // event is never consumed at origin :D + dataHolder: navEvent?.dataHolder ?? nil + ) + PageInfoManager.shared.setPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID, value: navigationEvent) + } } diff --git a/AltWidget/Intents/WidgetUpdateIntent.swift b/AltWidget/Intents/WidgetUpdateIntent.swift index 61d0c781..503c5e3e 100644 --- a/AltWidget/Intents/WidgetUpdateIntent.swift +++ b/AltWidget/Intents/WidgetUpdateIntent.swift @@ -10,6 +10,7 @@ import AppIntents @available(iOS 17, *) final class WidgetUpdateIntent: WidgetConfigurationIntent, @unchecked Sendable { + public static let COMMON_WIDGET_ID = 1 static var title: LocalizedStringResource { "Widget ID update Intent" } static var isDiscoverable: Bool { false } diff --git a/AltWidget/Manager/PageInfoManager.swift b/AltWidget/Manager/PageInfoManager.swift index a06e4af5..1c629d35 100644 --- a/AltWidget/Manager/PageInfoManager.swift +++ b/AltWidget/Manager/PageInfoManager.swift @@ -6,22 +6,32 @@ // Copyright © 2025 SideStore. All rights reserved. // -// This is a utility class +import Foundation + +// TODO: See if we can persist these values instead of keeping in memory to prevent memory leaks +// Possible ways: Userdefaults.standard - set/get ? class PageInfoManager { static var shared = PageInfoManager() - private var pageInfoMap: [Int: NavigationEvent] = [:] + private var pageInfoMap: [String: NavigationEvent] = [:] private init() {} - func setPageInfo(for key: Int, value: NavigationEvent?) { + private func getKey(forWidgetKind kind: String, forWidgetID id: Int) -> String{ + return "\(kind)@\(id)" + } + + func setPageInfo(forWidgetKind kind: String, forWidgetID id: Int, value: NavigationEvent?) { + let key = getKey(forWidgetKind: kind, forWidgetID: id) pageInfoMap[key] = value } - func getPageInfo(for key: Int) -> NavigationEvent? { - return pageInfoMap[key] + func getPageInfo(forWidgetKind kind: String, forWidgetID id: Int) -> NavigationEvent? { + let key = getKey(forWidgetKind: kind, forWidgetID: id) + return pageInfoMap[key] } - func popPageInfo(for key: Int) -> NavigationEvent? { + func popPageInfo(forWidgetKind kind: String, forWidgetID id: Int) -> NavigationEvent? { + let key = getKey(forWidgetKind: kind, forWidgetID: id) return pageInfoMap.removeValue(forKey: key) } diff --git a/AltWidget/Providers/ActiveAppsTimelineProvider.swift b/AltWidget/Providers/ActiveAppsTimelineProvider.swift index 148fecdd..7bbf3178 100644 --- a/AltWidget/Providers/ActiveAppsTimelineProvider.swift +++ b/AltWidget/Providers/ActiveAppsTimelineProvider.swift @@ -18,10 +18,16 @@ class ActiveAppsTimelineProvider: AppsTimelineProviderBase: AppsTimelineProviderBase [AppSnapshot] { var apps = apps + // if simulator, get the 10 simulated entries based on first entry #if targetEnvironment(simulator) apps = getSimulatedData(apps: apps) #endif - var currentPageApps = dataHolder.currentPage(inItems: apps) - if let widgetInfo = context, - let widgetID = widgetInfo.ID { - - var navEvent: NavigationEvent? = PageInfoManager.shared.getPageInfo(for: widgetID) - if let event = navEvent, - let direction = event.direction - { - // process navigation request only if event wasn't consumed yet - if !event.consumed { - switch (direction){ - case Direction.up: - currentPageApps = dataHolder.prevPage(inItems: apps, whenUnavailable: .current)! - case Direction.down: - currentPageApps = dataHolder.nextPage(inItems: apps, whenUnavailable: .current)! - } - // mark the event as consumed - // this prevents duplicate getUpdatedData() requests for same navigation event - navEvent!.consumed = true - } - } - PageInfoManager.shared.setPageInfo(for: widgetID, value: navEvent) + // always first page since this is never updated + var currentPageApps = defaultDataHolder.currentPage(inItems: apps) + + guard let widgetInfo = context, + let widgetID = widgetInfo.ID else + { + return currentPageApps } - + + let navEvent = getPageInfo(widgetID: widgetID) + guard var navEvent = navEvent, + let direction = navEvent.direction else + { + // when widget is edited for new ID than the current, + // buttons were never triggered for this ID, + // hence nav-event or direction wasn't set yet + updatePageInfo( + widgetID: widgetID, + navEvent: NavigationEvent(direction: nil, consumed: true, dataHolder: PaginationDataHolder(other: defaultDataHolder)) + ) + return currentPageApps + } + + let dataHolder = navEvent.dataHolder! + + // process navigation request only if event wasn't consumed yet + if !navEvent.consumed { + switch (direction){ + case Direction.up: + currentPageApps = dataHolder.prevPage(inItems: apps, whenUnavailable: .current)! + case Direction.down: + currentPageApps = dataHolder.nextPage(inItems: apps, whenUnavailable: .current)! + } + // mark the event as consumed + // this prevents duplicate getUpdatedData() requests for same navigation event + navEvent.consumed = true + }else{ + // since the event was consumed, get the current page as-is for this dataholder + currentPageApps = dataHolder.currentPage(inItems: apps) + } + + // put back the data + updatePageInfo(widgetID: widgetID, navEvent: navEvent) return currentPageApps } + + + private func getPageInfo(widgetID: Int) -> NavigationEvent?{ + return PageInfoManager.shared.getPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID) + } + + private func updatePageInfo(widgetID: Int, navEvent: NavigationEvent?) { + PageInfoManager.shared.setPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID, value: navEvent) + } } /// TimelineProvider for WidgetAppIntentConfiguration widget type diff --git a/AltWidget/Widgets/ActiveAppsWidget.swift b/AltWidget/Widgets/ActiveAppsWidget.swift index e380ff82..7ae561a0 100644 --- a/AltWidget/Widgets/ActiveAppsWidget.swift +++ b/AltWidget/Widgets/ActiveAppsWidget.swift @@ -32,16 +32,23 @@ struct ActiveAppsWidget: Widget static let MAX_ROWS_PER_PAGE: UInt = 3 } - let widgetKind = "ActiveApps - \(UUID().uuidString)" - + private static var id: Int = 1 + private let widgetKind: String + + init(){ + widgetKind = "ActiveApps - \(Self.id)" + Self.id += 1 + } + public var body: some WidgetConfiguration { + if #available(iOS 17, *) { let widgetConfig = AppIntentConfiguration( kind: widgetKind, intent: WidgetUpdateIntent.self, - provider: ActiveAppsTimelineProvider() + provider: ActiveAppsTimelineProvider(widgetKind: widgetKind) ) { entry in ActiveAppsWidgetView(entry: entry, widgetKind: widgetKind) } @@ -97,129 +104,106 @@ private struct ActiveAppsWidgetView: View GeometryReader { (geometry) in HStack(alignment: .center) { - appsListView(reader: geometry) + let itemsPerPage = ActiveAppsWidget.Constants.MAX_ROWS_PER_PAGE + + let preferredRowHeight = (geometry.size.height / Double(itemsPerPage)) - 8 + let rowHeight = min(preferredRowHeight, geometry.size.height / 2) + + LazyVStack(spacing: 12) { + ForEach(Array(entry.apps.enumerated()), id: \.offset) { index, app in + + let icon: UIImage = app.icon ?? UIImage(named: "SideStore")! + + // 1024x1024 images are not supported by previews but supported by device + // so we scale the image to 97% so as to reduce its actual size but not too much + // to somewhere below value, acceptable by previews ie < 1042x948 + let scalingFactor = 0.97 + + let resizedSize = CGSize( + width: icon.size.width * scalingFactor, + height: icon.size.height * scalingFactor + ) + + let resizedIcon = icon.resizing(to: resizedSize)! + let cornerRadius = rowHeight / 5.0 + let daysRemaining = app.expirationDate.numberOfCalendarDays(since: entry.date) + + HStack(spacing: 10) { + Image(uiImage: resizedIcon) + .resizable() + .aspectRatio(contentMode: .fit) + .cornerRadius(cornerRadius) + + + VStack(alignment: .leading, spacing: 1) { + Text(app.name) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + + let text = if entry.date > app.expirationDate + { + Text("Expired") + } + else + { + Text("Expires in \(daysRemaining) ") + (daysRemaining == 1 ? Text("day") : Text("days")) + } + + text + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .foregroundStyle(.secondary) + } + + Spacer() + + Countdown(startDate: app.refreshedDate, + endDate: app.expirationDate, + currentDate: entry.date, + strokeWidth: 3.0) // Slightly thinner circle stroke width + .background { + Color.black.opacity(0.1) + .mask(Capsule()) + .padding(.all, -5) + } + .font(.system(size: 16, weight: .semibold, design: .rounded)) + // this modifier invalidates the view (disables user interaction and shows a blinking effect) + .invalidatableContent() + .activatesRefreshAllAppsIntent() + + } + .frame(height: rowHeight) + + } + } Spacer(minLength: 16) - navigationBarView() + let buttonWidth: CGFloat = 16 + let widgetID = entry.context?.ID + + VStack { + Image(systemName: "arrow.up") + .resizable() + .frame(width: buttonWidth, height: buttonWidth) + .opacity(0.3) + // .mask(Capsule()) + .pageUpButton(widgetID, widgetKind) + + Spacer() + + Image(systemName: "arrow.down") + .resizable() + .frame(width: buttonWidth, height: buttonWidth) + .opacity(0.3) + // .mask(Capsule()) + .pageDownButton(widgetID, widgetKind) + } + .padding(.vertical) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } - private func appsListView(reader: GeometryProxy) -> some View { - let itemsPerPage = ActiveAppsWidget.Constants.MAX_ROWS_PER_PAGE - - let preferredRowHeight = (reader.size.height / Double(itemsPerPage)) - 8 - let rowHeight = min(preferredRowHeight, reader.size.height / 2) - - return LazyVStack(spacing: 12) { - ForEach(Array(entry.apps.enumerated()), id: \.offset) { index, app in - - appEntryRowView(app: app, rowHeight: rowHeight) - - } - } - } - - - private func appEntryRowView(app: AppSnapshot, rowHeight: Double) -> some View { - let icon: UIImage = app.icon ?? UIImage(named: "SideStore")! - - // 1024x1024 images are not supported by previews but supported by device - // so we scale the image to 97% so as to reduce its actual size but not too much - // to somewhere below value, acceptable by previews ie < 1042x948 - let scalingFactor = 0.97 - - let resizedSize = CGSize( - width: icon.size.width * scalingFactor, - height: icon.size.height * scalingFactor - ) - - let resizedIcon = icon.resizing(to: resizedSize)! - let cornerRadius = rowHeight / 5.0 - - return HStack(spacing: 10) { - Image(uiImage: resizedIcon) - .resizable() - .aspectRatio(contentMode: .fit) - .cornerRadius(cornerRadius) - - appDetailsView(app) - - Spacer() - - countDownView(app) - - } - .frame(height: rowHeight) - } - - - private func appDetailsView(_ app: AppSnapshot) -> some View { - let daysRemaining = app.expirationDate.numberOfCalendarDays(since: entry.date) - - return VStack(alignment: .leading, spacing: 1) { - Text(app.name) - .font(.system(size: 15, weight: .semibold, design: .rounded)) - - let text = if entry.date > app.expirationDate - { - Text("Expired") - } - else - { - Text("Expires in \(daysRemaining) ") + (daysRemaining == 1 ? Text("day") : Text("days")) - } - - text - .font(.system(size: 13, weight: .semibold, design: .rounded)) - .foregroundStyle(.secondary) - } - } - - private func countDownView(_ app: AppSnapshot) -> some View { - Countdown(startDate: app.refreshedDate, - endDate: app.expirationDate, - currentDate: entry.date, - strokeWidth: 3.0) // Slightly thinner circle stroke width - .background { - Color.black.opacity(0.1) - .mask(Capsule()) - .padding(.all, -5) - } - .font(.system(size: 16, weight: .semibold, design: .rounded)) - // this modifier invalidates the view (disables user interaction and shows a blinking effect) - .invalidatableContent() - .activatesRefreshAllAppsIntent() - - } - - private func navigationBarView() -> some View { - let buttonWidth: CGFloat = 16 - let widgetID = entry.context?.ID - - return VStack { - Image(systemName: "arrow.up") - .resizable() - .frame(width: buttonWidth, height: buttonWidth) - .opacity(0.3) - // .mask(Capsule()) - .pageUpButton(widgetID, widgetKind) - - Spacer() - - Image(systemName: "arrow.down") - .resizable() - .frame(width: buttonWidth, height: buttonWidth) - .opacity(0.3) - // .mask(Capsule()) - .pageDownButton(widgetID, widgetKind) - } - .padding(.vertical) - } - private var placeholder: some View { Text("App Not Found") .font(.system(.body, design: .rounded)) diff --git a/SideStore/Utils/pagination/PaginationDataHolder.swift b/SideStore/Utils/pagination/PaginationDataHolder.swift index 97231e4b..4412f289 100644 --- a/SideStore/Utils/pagination/PaginationDataHolder.swift +++ b/SideStore/Utils/pagination/PaginationDataHolder.swift @@ -18,6 +18,11 @@ public class PaginationDataHolder { self.currentPageindex = startPageIndex } + init(other: PaginationDataHolder) { + self.itemsPerPage = other.itemsPerPage + self.currentPageindex = other.currentPageindex + } + public enum PageLimitResult{ case null case empty