From d83e3d56d88fea4bb28cd892b5bf0a2d229dae23 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Fri, 19 Dec 2025 10:56:55 -0300 Subject: [PATCH 1/5] Adds watch complication support Adds a new WidgetWatch extension to provide complication support on watchOS. This allows users to quickly access MacMagazine news directly from their watch face. Adds deep linking support to open specific posts on the watchOS app when tapped from the complication. Persists the latest news snapshot on a shared location to be read by the complication. --- .../MacMagazine.xcodeproj/project.pbxproj | 197 +++++++++++- MacMagazine/WatchApp/MainApp/WatchApp.swift | 22 ++ .../MainApp/WatchNotificationsDelegate.swift | 55 ++++ .../ViewModel/FeedMainViewModel.swift | 61 +++- MacMagazine/WatchApp/Views/FeedMainView.swift | 12 +- MacMagazine/WatchApp/WatchApp.entitlements | 4 + MacMagazine/WatchApp/WatchWidgetShared.swift | 42 +++ MacMagazine/WidgetWatch/AppIntent.swift | 7 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../WidgetWatch/Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 + .../logo_color.imageset/Contents.json | 12 + .../logo_color.imageset/logo@3x 3.png | Bin 0 -> 10892 bytes .../logo_white.imageset/Contents.json | 12 + .../logo_white.imageset/logo.png | Bin 0 -> 2151 bytes MacMagazine/WidgetWatch/Info.plist | 11 + MacMagazine/WidgetWatch/WidgetWatch.swift | 301 ++++++++++++++++++ .../WidgetWatch/WidgetWatchBundle.swift | 9 + MacMagazine/WidgetWatchExtension.entitlements | 10 + 20 files changed, 785 insertions(+), 11 deletions(-) create mode 100644 MacMagazine/WatchApp/MainApp/WatchNotificationsDelegate.swift create mode 100644 MacMagazine/WatchApp/WatchWidgetShared.swift create mode 100644 MacMagazine/WidgetWatch/AppIntent.swift create mode 100644 MacMagazine/WidgetWatch/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 MacMagazine/WidgetWatch/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 MacMagazine/WidgetWatch/Assets.xcassets/Contents.json create mode 100644 MacMagazine/WidgetWatch/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/Contents.json create mode 100644 MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/logo@3x 3.png create mode 100644 MacMagazine/WidgetWatch/Assets.xcassets/logo_white.imageset/Contents.json create mode 100644 MacMagazine/WidgetWatch/Assets.xcassets/logo_white.imageset/logo.png create mode 100644 MacMagazine/WidgetWatch/Info.plist create mode 100644 MacMagazine/WidgetWatch/WidgetWatch.swift create mode 100644 MacMagazine/WidgetWatch/WidgetWatchBundle.swift create mode 100644 MacMagazine/WidgetWatchExtension.entitlements diff --git a/MacMagazine/MacMagazine.xcodeproj/project.pbxproj b/MacMagazine/MacMagazine.xcodeproj/project.pbxproj index 8c18deab..75debdcf 100644 --- a/MacMagazine/MacMagazine.xcodeproj/project.pbxproj +++ b/MacMagazine/MacMagazine.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 88C8B2E72EF476E100044DE9 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9BDD424E2EE9E82F00A9BD85 /* WidgetKit.framework */; }; + 88C8B2E82EF476E100044DE9 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9BDD42502EE9E82F00A9BD85 /* SwiftUI.framework */; }; + 88C8B2F72EF476E200044DE9 /* WidgetWatchExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 88C8B2E62EF476E100044DE9 /* WidgetWatchExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 88C8B2FE2EF48DC500044DE9 /* FeedLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 88C8B2FD2EF48DC500044DE9 /* FeedLibrary */; }; 9B024FDC2EC2719700D66056 /* SettingsLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 9B024FDB2EC2719700D66056 /* SettingsLibrary */; }; 9B1A43032EE0C14E00E56784 /* PodcastLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1A43022EE0C14E00E56784 /* PodcastLibrary */; }; 9B4F666F2EE9F223008ACACB /* FeedLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 9B4F666E2EE9F223008ACACB /* FeedLibrary */; }; @@ -22,6 +26,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 88C8B2F52EF476E200044DE9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9B9E9DBC2E986F8400255820 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 88C8B2E52EF476E100044DE9; + remoteInfo = WidgetWatchExtension; + }; 9BA828AA2EEC7AAB00F26FCB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9B9E9DBC2E986F8400255820 /* Project object */; @@ -39,6 +50,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 88C8B2FC2EF476E200044DE9 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 88C8B2F72EF476E200044DE9 /* WidgetWatchExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 9BA828AD2EEC7AAB00F26FCB /* Embed Watch Content */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -64,6 +86,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 88C8B2E62EF476E100044DE9 /* WidgetWatchExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetWatchExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 88C8B3012EF4AE7400044DE9 /* WidgetWatchExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetWatchExtension.entitlements; sourceTree = ""; }; 9B024FDA2EC2717200D66056 /* SettingsLibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SettingsLibrary; sourceTree = ""; }; 9B1A43012EE0C04D00E56784 /* PodcastLibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PodcastLibrary; sourceTree = ""; }; 9B334E912EE43FE4009F4947 /* MMLiveLibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MMLiveLibrary; sourceTree = ""; }; @@ -80,6 +104,20 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 88C8B2FB2EF476E200044DE9 /* Exceptions for "WidgetWatch" folder in "WidgetWatchExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 88C8B2E52EF476E100044DE9 /* WidgetWatchExtension */; + }; + 88C8B3052EF577EE00044DE9 /* Exceptions for "WatchApp" folder in "WidgetWatchExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + WatchWidgetShared.swift, + ); + target = 88C8B2E52EF476E100044DE9 /* WidgetWatchExtension */; + }; 9B9E9DD12E986F8500255820 /* Exceptions for "MacMagazine" folder in "MacMagazine" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -104,6 +142,14 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + 88C8B2E92EF476E100044DE9 /* WidgetWatch */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 88C8B2FB2EF476E200044DE9 /* Exceptions for "WidgetWatch" folder in "WidgetWatchExtension" target */, + ); + path = WidgetWatch; + sourceTree = ""; + }; 9B9E9DC62E986F8400255820 /* MacMagazine */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -115,6 +161,9 @@ }; 9BA8288D2EEC7AAA00F26FCB /* WatchApp */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 88C8B3052EF577EE00044DE9 /* Exceptions for "WatchApp" folder in "WidgetWatchExtension" target */, + ); path = WatchApp; sourceTree = ""; }; @@ -129,6 +178,16 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + 88C8B2E32EF476E100044DE9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 88C8B2FE2EF48DC500044DE9 /* FeedLibrary in Frameworks */, + 88C8B2E82EF476E100044DE9 /* SwiftUI.framework in Frameworks */, + 88C8B2E72EF476E100044DE9 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9B9E9DC12E986F8400255820 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -166,11 +225,13 @@ 9B9E9DBB2E986F8400255820 = { isa = PBXGroup; children = ( + 88C8B3012EF4AE7400044DE9 /* WidgetWatchExtension.entitlements */, 9BB3888E2EE0B2E4007AC71C /* MacMagazine.xctestplan */, 9B9E9DDA2E98701C00255820 /* Features */, 9B9E9DC62E986F8400255820 /* MacMagazine */, 9BDD42522EE9E82F00A9BD85 /* Widget */, 9BA8288D2EEC7AAA00F26FCB /* WatchApp */, + 88C8B2E92EF476E100044DE9 /* WidgetWatch */, 9B9E9DDE2E98709000255820 /* Frameworks */, 9B9E9DC52E986F8400255820 /* Products */, ); @@ -182,6 +243,7 @@ 9B9E9DC42E986F8400255820 /* MacMagazine.app */, 9BDD424D2EE9E82F00A9BD85 /* WidgetExtension.appex */, 9BA8288C2EEC7AAA00F26FCB /* WatchApp.app */, + 88C8B2E62EF476E100044DE9 /* WidgetWatchExtension.appex */, ); name = Products; sourceTree = ""; @@ -212,6 +274,29 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 88C8B2E52EF476E100044DE9 /* WidgetWatchExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 88C8B2F82EF476E200044DE9 /* Build configuration list for PBXNativeTarget "WidgetWatchExtension" */; + buildPhases = ( + 88C8B2E22EF476E100044DE9 /* Sources */, + 88C8B2E32EF476E100044DE9 /* Frameworks */, + 88C8B2E42EF476E100044DE9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 88C8B2E92EF476E100044DE9 /* WidgetWatch */, + ); + name = WidgetWatchExtension; + packageProductDependencies = ( + 88C8B2FD2EF48DC500044DE9 /* FeedLibrary */, + ); + productName = WidgetWatchExtension; + productReference = 88C8B2E62EF476E100044DE9 /* WidgetWatchExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 9B9E9DC32E986F8400255820 /* MacMagazine */ = { isa = PBXNativeTarget; buildConfigurationList = 9B9E9DD22E986F8500255820 /* Build configuration list for PBXNativeTarget "MacMagazine" */; @@ -252,10 +337,12 @@ 9BA828882EEC7AAA00F26FCB /* Sources */, 9BA828892EEC7AAA00F26FCB /* Frameworks */, 9BA8288A2EEC7AAA00F26FCB /* Resources */, + 88C8B2FC2EF476E200044DE9 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 88C8B2F62EF476E200044DE9 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 9BA8288D2EEC7AAA00F26FCB /* WatchApp */, @@ -298,9 +385,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 2610; + LastSwiftUpdateCheck = 2620; LastUpgradeCheck = 2610; TargetAttributes = { + 88C8B2E52EF476E100044DE9 = { + CreatedOnToolsVersion = 26.2; + }; 9B9E9DC32E986F8400255820 = { CreatedOnToolsVersion = 26.0.1; }; @@ -331,11 +421,19 @@ 9B9E9DC32E986F8400255820 /* MacMagazine */, 9BDD424C2EE9E82F00A9BD85 /* WidgetExtension */, 9BA8288B2EEC7AAA00F26FCB /* WatchApp */, + 88C8B2E52EF476E100044DE9 /* WidgetWatchExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 88C8B2E42EF476E100044DE9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9B9E9DC22E986F8400255820 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -382,6 +480,13 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 88C8B2E22EF476E100044DE9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9B9E9DC02E986F8400255820 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -406,6 +511,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 88C8B2F62EF476E200044DE9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 88C8B2E52EF476E100044DE9 /* WidgetWatchExtension */; + targetProxy = 88C8B2F52EF476E200044DE9 /* PBXContainerItemProxy */; + }; 9BA828AB2EEC7AAB00F26FCB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 9BA8288B2EEC7AAA00F26FCB /* WatchApp */; @@ -419,6 +529,74 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 88C8B2F92EF476E200044DE9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = WidgetWatchExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5.1.0; + DEVELOPMENT_TEAM = A5VW9QUF9L; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetWatch/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MacMagazine; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 5.1; + PRODUCT_BUNDLE_IDENTIFIER = com.brit.beta.macmagazine.watchkitapp.WidgetWatch; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 26.0; + }; + name = Debug; + }; + 88C8B2FA2EF476E200044DE9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = WidgetWatchExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5.1.0; + DEVELOPMENT_TEAM = A5VW9QUF9L; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetWatch/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MacMagazine; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 5.1; + PRODUCT_BUNDLE_IDENTIFIER = com.brit.beta.macmagazine.watchkitapp.WidgetWatch; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 26.0; + }; + name = Release; + }; 9B9E9DD32E986F8500255820 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -647,7 +825,7 @@ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 26.1; + WATCHOS_DEPLOYMENT_TARGET = 26.0; }; name = Debug; }; @@ -682,7 +860,7 @@ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 26.1; + WATCHOS_DEPLOYMENT_TARGET = 26.0; }; name = Release; }; @@ -751,6 +929,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 88C8B2F82EF476E200044DE9 /* Build configuration list for PBXNativeTarget "WidgetWatchExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 88C8B2F92EF476E200044DE9 /* Debug */, + 88C8B2FA2EF476E200044DE9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9B9E9DBF2E986F8400255820 /* Build configuration list for PBXProject "MacMagazine" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -790,6 +977,10 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 88C8B2FD2EF48DC500044DE9 /* FeedLibrary */ = { + isa = XCSwiftPackageProductDependency; + productName = FeedLibrary; + }; 9B024FDB2EC2719700D66056 /* SettingsLibrary */ = { isa = XCSwiftPackageProductDependency; productName = SettingsLibrary; diff --git a/MacMagazine/WatchApp/MainApp/WatchApp.swift b/MacMagazine/WatchApp/MainApp/WatchApp.swift index a2e87953..b8a424b5 100644 --- a/MacMagazine/WatchApp/MainApp/WatchApp.swift +++ b/MacMagazine/WatchApp/MainApp/WatchApp.swift @@ -2,12 +2,17 @@ import FeedLibrary import StorageLibrary import SwiftData import SwiftUI +import UserNotifications +import WatchKit @main struct WatchApp: App { private let database = Database(models: [FeedDB.self], inMemory: false) + @WKApplicationDelegateAdaptor(WatchNotificationsDelegate.self) + private var notifDelegate + var body: some Scene { WindowGroup { FeedMainView( @@ -16,6 +21,23 @@ struct WatchApp: App { ) ) .modelContainer(database.sharedModelContainer) + .onOpenURL { url in + handleDeepLink(url) + } + } + } + + @MainActor + private func handleDeepLink(_ url: URL) { + guard url.scheme == "macmagazine" else { return } + + let host = url.host ?? "" + let pathComponents = url.pathComponents.filter { $0 != "/" } + + guard host == "news" else { return } + + if pathComponents.count >= 2, pathComponents[0] == "post" { + _ = pathComponents[1] } } } diff --git a/MacMagazine/WatchApp/MainApp/WatchNotificationsDelegate.swift b/MacMagazine/WatchApp/MainApp/WatchNotificationsDelegate.swift new file mode 100644 index 00000000..a1cde677 --- /dev/null +++ b/MacMagazine/WatchApp/MainApp/WatchNotificationsDelegate.swift @@ -0,0 +1,55 @@ +import Foundation +import UserNotifications +import WatchKit +import WidgetKit + +final class WatchNotificationsDelegate: NSObject, + WKApplicationDelegate, + UNUserNotificationCenterDelegate { + + func applicationDidFinishLaunching() { + UNUserNotificationCenter.current().delegate = self + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse + ) async { + handlePush(userInfo: response.notification.request.content.userInfo) + } + + // MARK: - Push handling + + private func handlePush(userInfo: [AnyHashable: Any]) { + guard + let postId = userInfo["postId"] as? String, + let title = userInfo["title"] as? String, + !postId.isEmpty, + !title.isEmpty + else { + return + } + + let date: Date = { + if let time = userInfo["date"] as? TimeInterval { + return Date(timeIntervalSince1970: time) + } + if let iso = userInfo["date"] as? String, + let date = ISO8601DateFormatter().date(from: iso) { + return date + } + return Date() + }() + + MacMagazineWidgetSharedStore.write( + snapshot: .init( + postId: postId, + title: title, + date: date + ) + ) + + // Atualiza as complicações + WidgetCenter.shared.reloadTimelines(ofKind: "WidgetWatch") + } +} diff --git a/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift b/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift index 97e7a344..e806659f 100644 --- a/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift +++ b/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift @@ -4,6 +4,7 @@ import Foundation import StorageLibrary import SwiftData import WatchKit +import WidgetKit @MainActor final class FeedMainViewModel: ObservableObject { @@ -21,8 +22,11 @@ final class FeedMainViewModel: ObservableObject { private let feedViewModel: FeedViewModel private var didLoadInitial: Bool = false + private var lastRefreshAt: Date? + private let staleInterval: TimeInterval = 30 * 60 // MARK: - Init + init(feedViewModel: FeedViewModel) { self.feedViewModel = feedViewModel status = feedViewModel.status @@ -34,24 +38,40 @@ final class FeedMainViewModel: ObservableObject { guard !didLoadInitial else { return } didLoadInitial = true + // Sempre tenta refletir o estado local imediatamente if hasItems { status = .done - return + persistLatestPostSnapshot(from: modelContext) } - await refresh(modelContext: modelContext) + // Regra de auto-refresh: + // - Se não tem itens -> atualiza + // - Se tem itens, atualiza somente se estiver "stale" + let shouldAutoRefresh: Bool = { + if !hasItems { return true } + guard let last = lastRefreshAt else { return true } // nunca atualizou nesta sessão + return Date().timeIntervalSince(last) > staleInterval + }() + + if shouldAutoRefresh { + await refresh(modelContext: modelContext) + } } func refresh(modelContext: ModelContext) async { guard !isRefreshing else { return } isRefreshing = true - - defer { - isRefreshing = false - } + defer { isRefreshing = false } _ = try? await feedViewModel.getWatchFeed() status = feedViewModel.status + lastRefreshAt = Date() + + // Persiste snapshot do banco (fonte de verdade) + persistLatestPostSnapshot(from: modelContext) + + // Atualiza a complication + WidgetCenter.shared.reloadTimelines(ofKind: "WidgetWatch") } func toggleFavorite(post: FeedDB, modelContext: ModelContext) { @@ -60,6 +80,25 @@ final class FeedMainViewModel: ObservableObject { showActions = true } + // MARK: - Snapshot para Widget + + private func persistLatestPostSnapshot(from modelContext: ModelContext) { + var descriptor = FetchDescriptor( + sortBy: [SortDescriptor(\.pubDate, order: .reverse)] + ) + descriptor.fetchLimit = 1 + + guard let last = try? modelContext.fetch(descriptor).first else { return } + + MacMagazineWidgetSharedStore.write( + snapshot: .init( + postId: last.postId, + title: last.title, + date: last.pubDate + ) + ) + } + // MARK: - Index / Helpers func computeSelectedIndexByMidY(items: [FeedDB], positions: [String: CGPoint]) -> Int { @@ -88,8 +127,16 @@ final class FeedMainViewModel: ObservableObject { return min(max(index, 0), quantity - 1) } - @MainActor func setStatusForPreview(_ status: FeedViewModel.Status) { self.status = status } + + func openPost(withId postId: String, modelContext: ModelContext) { + let predicate = #Predicate { $0.postId == postId } + let descriptor = FetchDescriptor(predicate: predicate) + + if let post = try? modelContext.fetch(descriptor).first { + selectedPostForDetail = SelectedPost(post: post) + } + } } diff --git a/MacMagazine/WatchApp/Views/FeedMainView.swift b/MacMagazine/WatchApp/Views/FeedMainView.swift index 2d6bb905..8f1b70a5 100644 --- a/MacMagazine/WatchApp/Views/FeedMainView.swift +++ b/MacMagazine/WatchApp/Views/FeedMainView.swift @@ -38,7 +38,7 @@ struct FeedMainView: View { NavigationStack { rootContent .navigationBarTitleDisplayMode(.inline) - .task(id: items.count) { + .task { guard !isRunningForPreviews else { return } await viewModel.loadInitialIfNeeded( hasItems: !items.isEmpty, @@ -48,6 +48,16 @@ struct FeedMainView: View { .navigationDestination(item: $viewModel.selectedPostForDetail) { payload in FeedDetailView(viewModel: viewModel, post: payload.post) } + .onOpenURL { url in + guard url.scheme == "macmagazine" else { return } + guard url.host == "news" else { return } + + let parts = url.pathComponents.filter { $0 != "/" } + if parts.count >= 2, parts[0] == "post" { + let postId = parts[1] + viewModel.openPost(withId: postId, modelContext: modelContext) + } + } } } diff --git a/MacMagazine/WatchApp/WatchApp.entitlements b/MacMagazine/WatchApp/WatchApp.entitlements index 2284d00f..a44bc9b3 100644 --- a/MacMagazine/WatchApp/WatchApp.entitlements +++ b/MacMagazine/WatchApp/WatchApp.entitlements @@ -12,5 +12,9 @@ CloudKit + com.apple.security.application-groups + + group.com.brit.beta.macmagazine + diff --git a/MacMagazine/WatchApp/WatchWidgetShared.swift b/MacMagazine/WatchApp/WatchWidgetShared.swift new file mode 100644 index 00000000..33ed17de --- /dev/null +++ b/MacMagazine/WatchApp/WatchWidgetShared.swift @@ -0,0 +1,42 @@ +import Foundation + +enum MacMagazineWidgetSharedStore { + static let appGroupID = "group.com.brit.beta.macmagazine" + + private enum Keys { + static let lastPostTitle = "macmagazine.widget.lastPost.title" + static let lastPostDate = "macmagazine.widget.lastPost.date" + static let lastPostId = "macmagazine.widget.lastPost.id" + } + + struct Snapshot: Equatable { + let postId: String + let title: String + let date: Date + } + + static func write(snapshot: Snapshot) { + guard let defaults = UserDefaults(suiteName: appGroupID) else { return } + + defaults.set(snapshot.title, forKey: Keys.lastPostTitle) + defaults.set(snapshot.date, forKey: Keys.lastPostDate) + defaults.set(snapshot.postId, forKey: Keys.lastPostId) + defaults.synchronize() + } + + static func readSnapshot() -> Snapshot? { + guard let defaults = UserDefaults(suiteName: appGroupID) else { return nil } + + guard + let title = defaults.string(forKey: Keys.lastPostTitle), + let date = defaults.object(forKey: Keys.lastPostDate) as? Date, + let postId = defaults.string(forKey: Keys.lastPostId), + !title.isEmpty, + !postId.isEmpty + else { + return nil + } + + return Snapshot(postId: postId, title: title, date: date) + } +} diff --git a/MacMagazine/WidgetWatch/AppIntent.swift b/MacMagazine/WidgetWatch/AppIntent.swift new file mode 100644 index 00000000..d2680dbf --- /dev/null +++ b/MacMagazine/WidgetWatch/AppIntent.swift @@ -0,0 +1,7 @@ +import AppIntents +import WidgetKit + +struct ConfigurationAppIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource { "MacMagazine" } + static var description: IntentDescription { "Configuração da complicação." } +} diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/AccentColor.colorset/Contents.json b/MacMagazine/WidgetWatch/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/MacMagazine/WidgetWatch/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/AppIcon.appiconset/Contents.json b/MacMagazine/WidgetWatch/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..49c81cd8 --- /dev/null +++ b/MacMagazine/WidgetWatch/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/Contents.json b/MacMagazine/WidgetWatch/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/MacMagazine/WidgetWatch/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/WidgetBackground.colorset/Contents.json b/MacMagazine/WidgetWatch/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/MacMagazine/WidgetWatch/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/Contents.json b/MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/Contents.json new file mode 100644 index 00000000..085cd9e5 --- /dev/null +++ b/MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "logo@3x 3.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/logo@3x 3.png b/MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/logo@3x 3.png new file mode 100644 index 0000000000000000000000000000000000000000..63606da9d9c13dc9afe0d2f13bf9678e4d868370 GIT binary patch literal 10892 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 literal 0 HcmV?d00001 diff --git a/MacMagazine/WidgetWatch/Info.plist b/MacMagazine/WidgetWatch/Info.plist new file mode 100644 index 00000000..0f118fb7 --- /dev/null +++ b/MacMagazine/WidgetWatch/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/MacMagazine/WidgetWatch/WidgetWatch.swift b/MacMagazine/WidgetWatch/WidgetWatch.swift new file mode 100644 index 00000000..e28a762e --- /dev/null +++ b/MacMagazine/WidgetWatch/WidgetWatch.swift @@ -0,0 +1,301 @@ +import AppIntents +import SwiftUI +import WidgetKit + +// MARK: - Entry + +struct SimpleEntry: TimelineEntry { + let date: Date + let configuration: ConfigurationAppIntent + + let lastPostId: String? + let lastPostTitle: String + let lastPostDate: Date? +} + +// MARK: - Provider + +struct Provider: AppIntentTimelineProvider { + + func recommendations() -> [AppIntentRecommendation] { + [ + AppIntentRecommendation( + intent: ConfigurationAppIntent(), + description: "MacMagazine" + ) + ] + } + + func placeholder(in context: Context) -> SimpleEntry { + SimpleEntry( + date: .now, + configuration: ConfigurationAppIntent(), + lastPostId: UUID().uuidString, + lastPostTitle: "Apple lança atualização do watchOS", + lastPostDate: .now.addingTimeInterval(-60 * 25) + ) + } + + func snapshot( + for configuration: ConfigurationAppIntent, + in context: Context + ) async -> SimpleEntry { + makeEntry(configuration: configuration) + } + + func timeline( + for configuration: ConfigurationAppIntent, + in context: Context + ) async -> Timeline { + + let entry = makeEntry(configuration: configuration) + + let nextUpdate = + Calendar.current.date(byAdding: .minute, value: 30, to: .now) + ?? .now.addingTimeInterval(1800) + + return Timeline(entries: [entry], policy: .after(nextUpdate)) + } + + private func makeEntry(configuration: ConfigurationAppIntent) -> SimpleEntry { + let snap = MacMagazineWidgetSharedStore.readSnapshot() + + return SimpleEntry( + date: .now, + configuration: configuration, + lastPostId: snap?.postId, + lastPostTitle: snap?.title ?? "MacMagazine", + lastPostDate: snap?.date + ) + } +} + +// MARK: - Entry View + +struct WidgetWatchEntryView: View { + + let entry: SimpleEntry + @Environment(\.widgetFamily) private var family + @Environment(\.widgetRenderingMode) private var renderingMode + + var body: some View { + content + .containerBackground(for: .widget) { Color.clear } + } + + @ViewBuilder + private var content: some View { + switch family { + case .accessoryCircular: + circular + case .accessoryCorner: + corner + case .accessoryInline: + inline + case .accessoryRectangular: + rectangular + @unknown default: + EmptyView() + } + } +} + +// MARK: - Layouts + +private extension WidgetWatchEntryView { + + // ACCESSORY CIRCULAR + var circular: some View { + ZStack { + AccessoryWidgetBackground() + + Image("logo_color") + .resizable() + .scaledToFit() + .frame(width: 30, height: 30) + } + .widgetURL(URL(string: "macmagazine://news")) + .accessibilityLabel("Abrir MacMagazine") + } + + // ACCESSORY CORNER + var corner: some View { + Image(renderingMode == .fullColor ? "logo_color" : "logo_white") + .resizable() + .renderingMode(renderingMode == .fullColor ? .original : .template) + .scaledToFit() + .frame(width: 35, height: 35) + .foregroundStyle(.primary) + .widgetLabel { + Text(entry.lastPostTitle) + .lineLimit(1) + } + .widgetURL(widgetPostURL()) + .accessibilityElement(children: .ignore) + .accessibilityLabel("MacMagazine") + .accessibilityValue("Última notícia: \(entry.lastPostTitle)") + .accessibilityHint("Toque para abrir as notícias") + } + + // ACCESSORY INLINE + var inline: some View { + Text("\(entry.lastPostTitle)") + .widgetURL(widgetPostURL()) + .accessibilityLabel("MacMagazine") + .accessibilityValue("Última notícia: \(entry.lastPostTitle)") + .accessibilityHint("Toque para abrir as notícias") + } + + // ACCESSORY RECTANGULAR + var rectangular: some View { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 6) { + Image(renderingMode == .fullColor ? "logo_color" : "logo_white") + .resizable() + .renderingMode(renderingMode == .fullColor ? .original : .template) + .scaledToFit() + .frame(width: 18, height: 18) + .foregroundStyle(.primary) + + Text(relativePostTimeText(entry.lastPostDate)) + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(1) + + Spacer(minLength: 0) + } + + Text(entry.lastPostTitle) + .font(.callout) + .lineLimit(2) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .containerBackground(for: .widget) { AccessoryWidgetBackground() } + .widgetURL(widgetPostURL()) + .accessibilityElement(children: .combine) + .accessibilityLabel("MacMagazine") + .accessibilityValue(accessibilityValueForRectangular()) + .accessibilityHint("Toque para abrir as notícias") + } + + func widgetPostURL() -> URL? { + if let postId = entry.lastPostId, !postId.isEmpty { + return URL(string: "macmagazine://news/post/\(postId)") + } + + return URL(string: "macmagazine://news") + } + + func relativePostTimeText(_ date: Date?) -> String { + guard let date else { return "Atualizado recentemente" } + + let time = Self.timeFormatter.string(from: date) + let calendar = Calendar.current + + if calendar.isDateInToday(date) { + return "Hoje às \(time)" + } + + if calendar.isDateInYesterday(date) { + return "Ontem às \(time)" + } + + return "\(Self.dayFormatter.string(from: date)) às \(time)" + } + + func accessibilityValueForRectangular() -> String { + let whenText = relativePostTimeText(entry.lastPostDate) + return "\(whenText), \(entry.lastPostTitle)" + } + + static let timeFormatter: DateFormatter = { + let formatterDate = DateFormatter() + formatterDate.locale = Locale(identifier: "pt_BR") + formatterDate.dateFormat = "HH:mm" + return formatterDate + }() + + static let dayFormatter: DateFormatter = { + let formatterDate = DateFormatter() + formatterDate.locale = Locale(identifier: "pt_BR") + formatterDate.dateFormat = "dd/MM" + return formatterDate + }() +} + +// MARK: - Widget + +struct WidgetWatch: Widget { + + let kind: String = "WidgetWatch" + + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: kind, + intent: ConfigurationAppIntent.self, + provider: Provider() + ) { entry in + WidgetWatchEntryView(entry: entry) + } + .configurationDisplayName("MacMagazine") + .description("Acesso rápido às notícias do MacMagazine.") + .supportedFamilies([ + .accessoryCircular, + .accessoryCorner, + .accessoryInline, + .accessoryRectangular + ]) + } +} + +#if DEBUG + +#Preview("Circular", as: .accessoryCircular) { + WidgetWatch() +} timeline: { + SimpleEntry( + date: .now, + configuration: ConfigurationAppIntent(), + lastPostId: UUID().uuidString, + lastPostTitle: "Apple lança atualização do watchOS", + lastPostDate: .now.addingTimeInterval(-60 * 25) + ) +} + +#Preview("Corner", as: .accessoryCorner) { + WidgetWatch() +} timeline: { + SimpleEntry( + date: .now, + configuration: ConfigurationAppIntent(), + lastPostId: UUID().uuidString, + lastPostTitle: "Apple lança atualização do watchOS", + lastPostDate: .now.addingTimeInterval(-60 * 25) + ) +} + +#Preview("Inline", as: .accessoryInline) { + WidgetWatch() +} timeline: { + SimpleEntry( + date: .now, + configuration: ConfigurationAppIntent(), + lastPostId: UUID().uuidString, + lastPostTitle: "Apple lança atualização do watchOS", + lastPostDate: .now.addingTimeInterval(-60 * 25) + ) +} + +#Preview("Rectangular",as: .accessoryRectangular) { + WidgetWatch() +} timeline: { + SimpleEntry( + date: .now, + configuration: ConfigurationAppIntent(), + lastPostId: UUID().uuidString, + lastPostTitle: "O melhor pedaço da maçã da internet, clique para ver mais!", + lastPostDate: .now.addingTimeInterval(-60 * 90) + ) +} + +#endif diff --git a/MacMagazine/WidgetWatch/WidgetWatchBundle.swift b/MacMagazine/WidgetWatch/WidgetWatchBundle.swift new file mode 100644 index 00000000..b120048e --- /dev/null +++ b/MacMagazine/WidgetWatch/WidgetWatchBundle.swift @@ -0,0 +1,9 @@ +import SwiftUI +import WidgetKit + +@main +struct WidgetWatchBundle: WidgetBundle { + var body: some Widget { + WidgetWatch() + } +} diff --git a/MacMagazine/WidgetWatchExtension.entitlements b/MacMagazine/WidgetWatchExtension.entitlements new file mode 100644 index 00000000..1f2b1f33 --- /dev/null +++ b/MacMagazine/WidgetWatchExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.brit.beta.macmagazine + + + From 502ffddb6cbb3ae295965d1d52c9734fc8b6fed6 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Fri, 19 Dec 2025 11:00:55 -0300 Subject: [PATCH 2/5] Refactors feed refresh logic on watch Simplifies the feed refresh logic by removing redundant comments and streamlining the auto-refresh decision-making process. This improves code readability and maintainability while ensuring that the feed updates efficiently. --- MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift b/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift index e806659f..42177225 100644 --- a/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift +++ b/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift @@ -38,18 +38,14 @@ final class FeedMainViewModel: ObservableObject { guard !didLoadInitial else { return } didLoadInitial = true - // Sempre tenta refletir o estado local imediatamente if hasItems { status = .done persistLatestPostSnapshot(from: modelContext) } - // Regra de auto-refresh: - // - Se não tem itens -> atualiza - // - Se tem itens, atualiza somente se estiver "stale" let shouldAutoRefresh: Bool = { if !hasItems { return true } - guard let last = lastRefreshAt else { return true } // nunca atualizou nesta sessão + guard let last = lastRefreshAt else { return true } return Date().timeIntervalSince(last) > staleInterval }() @@ -67,10 +63,8 @@ final class FeedMainViewModel: ObservableObject { status = feedViewModel.status lastRefreshAt = Date() - // Persiste snapshot do banco (fonte de verdade) persistLatestPostSnapshot(from: modelContext) - // Atualiza a complication WidgetCenter.shared.reloadTimelines(ofKind: "WidgetWatch") } From 2024f29689529f0eef604bb4efaef228785ad971 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Fri, 19 Dec 2025 11:08:57 -0300 Subject: [PATCH 3/5] Refactors Watch app feed detail navigation Simplifies the navigation flow to the feed detail view by directly passing the `FeedDB` object, instead of a custom `SelectedPost` struct. This change avoids an unnecessary intermediate object, leading to a more straightforward data flow. --- .../ViewModel/FeedMainViewModel.swift | 6 +- MacMagazine/WatchApp/Views/FeedMainView.swift | 5 +- MacMagazine/WidgetWatch/WidgetWatch.swift | 86 +++++++++---------- 3 files changed, 48 insertions(+), 49 deletions(-) diff --git a/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift b/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift index cfb17b8e..57189732 100644 --- a/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift +++ b/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift @@ -125,9 +125,9 @@ final class FeedMainViewModel { let predicate = #Predicate { $0.postId == postId } let descriptor = FetchDescriptor(predicate: predicate) - if let post = try? modelContext.fetch(descriptor).first { - selectedPostForDetail = SelectedPost(post: post) - } + if let post = try? modelContext.fetch(descriptor).first { + selectedPostForDetail = post + } } } diff --git a/MacMagazine/WatchApp/Views/FeedMainView.swift b/MacMagazine/WatchApp/Views/FeedMainView.swift index 7e298709..6997b8a8 100644 --- a/MacMagazine/WatchApp/Views/FeedMainView.swift +++ b/MacMagazine/WatchApp/Views/FeedMainView.swift @@ -33,14 +33,13 @@ struct FeedMainView: View { rootContent .navigationBarTitleDisplayMode(.inline) .task { - guard !isRunningForPreviews else { return } await viewModel.loadInitialIfNeeded( hasItems: !items.isEmpty, modelContext: modelContext ) } - .navigationDestination(item: $viewModel.selectedPostForDetail) { payload in - FeedDetailView(viewModel: viewModel, post: payload) + .navigationDestination(item: $viewModel.selectedPostForDetail) { post in + FeedDetailView(viewModel: viewModel, post: post) } .onOpenURL { url in guard url.scheme == "macmagazine" else { return } diff --git a/MacMagazine/WidgetWatch/WidgetWatch.swift b/MacMagazine/WidgetWatch/WidgetWatch.swift index e28a762e..651cc208 100644 --- a/MacMagazine/WidgetWatch/WidgetWatch.swift +++ b/MacMagazine/WidgetWatch/WidgetWatch.swift @@ -7,7 +7,7 @@ import WidgetKit struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationAppIntent - + let lastPostId: String? let lastPostTitle: String let lastPostDate: Date? @@ -16,7 +16,7 @@ struct SimpleEntry: TimelineEntry { // MARK: - Provider struct Provider: AppIntentTimelineProvider { - + func recommendations() -> [AppIntentRecommendation] { [ AppIntentRecommendation( @@ -25,7 +25,7 @@ struct Provider: AppIntentTimelineProvider { ) ] } - + func placeholder(in context: Context) -> SimpleEntry { SimpleEntry( date: .now, @@ -35,31 +35,31 @@ struct Provider: AppIntentTimelineProvider { lastPostDate: .now.addingTimeInterval(-60 * 25) ) } - + func snapshot( for configuration: ConfigurationAppIntent, in context: Context ) async -> SimpleEntry { makeEntry(configuration: configuration) } - + func timeline( for configuration: ConfigurationAppIntent, in context: Context ) async -> Timeline { - + let entry = makeEntry(configuration: configuration) - + let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: .now) ?? .now.addingTimeInterval(1800) - + return Timeline(entries: [entry], policy: .after(nextUpdate)) } - + private func makeEntry(configuration: ConfigurationAppIntent) -> SimpleEntry { let snap = MacMagazineWidgetSharedStore.readSnapshot() - + return SimpleEntry( date: .now, configuration: configuration, @@ -73,29 +73,29 @@ struct Provider: AppIntentTimelineProvider { // MARK: - Entry View struct WidgetWatchEntryView: View { - + let entry: SimpleEntry @Environment(\.widgetFamily) private var family @Environment(\.widgetRenderingMode) private var renderingMode - + var body: some View { content .containerBackground(for: .widget) { Color.clear } } - + @ViewBuilder private var content: some View { switch family { - case .accessoryCircular: - circular - case .accessoryCorner: - corner - case .accessoryInline: - inline - case .accessoryRectangular: - rectangular - @unknown default: - EmptyView() + case .accessoryCircular: + circular + case .accessoryCorner: + corner + case .accessoryInline: + inline + case .accessoryRectangular: + rectangular + @unknown default: + EmptyView() } } } @@ -103,12 +103,12 @@ struct WidgetWatchEntryView: View { // MARK: - Layouts private extension WidgetWatchEntryView { - + // ACCESSORY CIRCULAR var circular: some View { ZStack { AccessoryWidgetBackground() - + Image("logo_color") .resizable() .scaledToFit() @@ -117,7 +117,7 @@ private extension WidgetWatchEntryView { .widgetURL(URL(string: "macmagazine://news")) .accessibilityLabel("Abrir MacMagazine") } - + // ACCESSORY CORNER var corner: some View { Image(renderingMode == .fullColor ? "logo_color" : "logo_white") @@ -136,7 +136,7 @@ private extension WidgetWatchEntryView { .accessibilityValue("Última notícia: \(entry.lastPostTitle)") .accessibilityHint("Toque para abrir as notícias") } - + // ACCESSORY INLINE var inline: some View { Text("\(entry.lastPostTitle)") @@ -145,7 +145,7 @@ private extension WidgetWatchEntryView { .accessibilityValue("Última notícia: \(entry.lastPostTitle)") .accessibilityHint("Toque para abrir as notícias") } - + // ACCESSORY RECTANGULAR var rectangular: some View { VStack(alignment: .leading, spacing: 4) { @@ -156,15 +156,15 @@ private extension WidgetWatchEntryView { .scaledToFit() .frame(width: 18, height: 18) .foregroundStyle(.primary) - + Text(relativePostTimeText(entry.lastPostDate)) .font(.footnote) .foregroundStyle(.secondary) .lineLimit(1) - + Spacer(minLength: 0) } - + Text(entry.lastPostTitle) .font(.callout) .lineLimit(2) @@ -177,44 +177,44 @@ private extension WidgetWatchEntryView { .accessibilityValue(accessibilityValueForRectangular()) .accessibilityHint("Toque para abrir as notícias") } - + func widgetPostURL() -> URL? { if let postId = entry.lastPostId, !postId.isEmpty { return URL(string: "macmagazine://news/post/\(postId)") } - + return URL(string: "macmagazine://news") } - + func relativePostTimeText(_ date: Date?) -> String { guard let date else { return "Atualizado recentemente" } - + let time = Self.timeFormatter.string(from: date) let calendar = Calendar.current - + if calendar.isDateInToday(date) { return "Hoje às \(time)" } - + if calendar.isDateInYesterday(date) { return "Ontem às \(time)" } - + return "\(Self.dayFormatter.string(from: date)) às \(time)" } - + func accessibilityValueForRectangular() -> String { let whenText = relativePostTimeText(entry.lastPostDate) return "\(whenText), \(entry.lastPostTitle)" } - + static let timeFormatter: DateFormatter = { let formatterDate = DateFormatter() formatterDate.locale = Locale(identifier: "pt_BR") formatterDate.dateFormat = "HH:mm" return formatterDate }() - + static let dayFormatter: DateFormatter = { let formatterDate = DateFormatter() formatterDate.locale = Locale(identifier: "pt_BR") @@ -226,9 +226,9 @@ private extension WidgetWatchEntryView { // MARK: - Widget struct WidgetWatch: Widget { - + let kind: String = "WidgetWatch" - + var body: some WidgetConfiguration { AppIntentConfiguration( kind: kind, From c2cc60005fc1ad1487de82e5222df4a183062ad3 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Fri, 19 Dec 2025 11:13:21 -0300 Subject: [PATCH 4/5] Adjusts UI element padding Refines the padding of elements in the feed and row views to improve visual appearance. The refresh control receives a slight padding adjustment. The trailing padding on the feed row is increased. --- MacMagazine/WatchApp/Views/FeedMainView.swift | 1 + MacMagazine/WatchApp/Views/Row/FeedRowView.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/MacMagazine/WatchApp/Views/FeedMainView.swift b/MacMagazine/WatchApp/Views/FeedMainView.swift index 6997b8a8..c525856b 100644 --- a/MacMagazine/WatchApp/Views/FeedMainView.swift +++ b/MacMagazine/WatchApp/Views/FeedMainView.swift @@ -210,6 +210,7 @@ struct FeedMainView: View { ) .frame(maxHeight: .infinity, alignment: .trailing) .opacity(viewModel.isRefreshing ? 0 : 1) + .padding(.trailing, 4) } .overlay { if viewModel.isRefreshing { diff --git a/MacMagazine/WatchApp/Views/Row/FeedRowView.swift b/MacMagazine/WatchApp/Views/Row/FeedRowView.swift index e5c1f128..184c65d8 100644 --- a/MacMagazine/WatchApp/Views/Row/FeedRowView.swift +++ b/MacMagazine/WatchApp/Views/Row/FeedRowView.swift @@ -62,7 +62,7 @@ struct FeedRowView: View { .foregroundStyle(.primary) .frame(maxWidth: .infinity, alignment: .leading) .lineLimit(3) - .padding(.trailing, 12) + .padding(.trailing, 22) Text(post.dateText) .font(.caption2) From 6388be6c44c8e57aca38543d9b4271e710bde84f Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Fri, 19 Dec 2025 11:17:55 -0300 Subject: [PATCH 5/5] Fixes typo in preview declaration Corrects a typographical error in the preview declaration by changing "as:" to "as:". --- MacMagazine/WidgetWatch/WidgetWatch.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MacMagazine/WidgetWatch/WidgetWatch.swift b/MacMagazine/WidgetWatch/WidgetWatch.swift index 651cc208..43b5d933 100644 --- a/MacMagazine/WidgetWatch/WidgetWatch.swift +++ b/MacMagazine/WidgetWatch/WidgetWatch.swift @@ -286,7 +286,7 @@ struct WidgetWatch: Widget { ) } -#Preview("Rectangular",as: .accessoryRectangular) { +#Preview("Rectangular", as: .accessoryRectangular) { WidgetWatch() } timeline: { SimpleEntry(