From d83e3d56d88fea4bb28cd892b5bf0a2d229dae23 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Fri, 19 Dec 2025 10:56:55 -0300 Subject: [PATCH 01/10] 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 zcmZ{KWmp|S((b|C2@u@fJ-EBO1P%n3gF6Iw5AG5if?IG75JGTwC%BV?+edcy?(TEH zduN`Ws;PS4s_yFkF*7|;YAUj5$b`rM002#1PDJw0&OyS<{$<5?6lmP%AS^yw06aaX5GX)+10PgGnz>x_6Admq7;5+5Cs|&q3LM`;< zEtQo4jBh$301gTp0Q06ny`2CkLcrf>ZyG=Wis;|E1{D3jFwg)%gbe`pUl@Zo`S*}} zYkxWalrRNQ|A|-t{cp5M0nER3`M{f#f8o?+O5*;QVb+fUFz>005fPMoZ64 zPgzO8%n`(DYVP>Sg4GM;^p^z?@)CGcK^AVNx)}>xpnwyQ~|A+S1^H178#`Vv1LVtq^ zsM&Z~*y~H#fGiwb-?}Ek%fT=7k2L?w^Pi6X7gO&)Onwgb|7QM|=l^C(I@&wBXgZmi zS%`4_i}GKl|3d%mw}7gPjm29@{}%2q{D0a0g%@J`Th;%n@;|5WAJ(@y6hRhZ`}cYf zLB5@~eOvo<-11W5T3%3VxxU{l7K}C)yjS;E1<%vTF1gToapY{0>BSIv&=L^Kr=#1$ zV0m!m6xFnJZEapFgz!$uq@lG7B&OA%863((21JE$wc(~ELgiy|umz)(Pu?d8VxHD}^>L=`tS0~s z|5o&J^`)Y%y7gA^4~uebTkQfJmWtFX16njxh3(4Fbv}>>TPM_q5;etJP^0Iw;I@f!Iw0 zfmaw#N#YcyNlEfGyGEgWjKY6QlWom7RwN-pzMD)lry}usbnSzDSFjq;6Y*z?F7M^0 zR99qjqvy58(n6nO!!~-JRB*+W2VEd&5U3)EAr{WC7**8-#V1Z1va9ezo~5g8iL}w& z?elKPe*D_wM6We3JBT*P1dpnxJ^6Pi2{1R~RnZ|~NaM!;>Q@L?pR@)_eqAb?dgz%gb;%wcJ#`_{tSf`TId5<<{|7-027u9L8kRX1O~bc8}Q+`<97K(l3w{Xg`;QMt~=M!dH;0MY~q)QZ8&mh zlbcMdkeicQ8&~-z>iMMS!E%9z34!@J3yW3Q3%(c(!5IFO6Bwf_$Y^Y0AN96gC zffr>y;Yk-)<~hLMPw%C|0~hy{5pQDcp|n)=j+f@F8o3XtqPv8TZ{s$#hph)8npcOs z*)qyVZokcHwem%^!e&;bi&mWb~aM&+Xchy0ec>>c+bj?+2lz5~ct^-+_ z&u3n4L0w%4zt3B8fGlCvMqJwxKPi|ljET^Jk+a9JCVl0zg!oJt{yX*5-i*p4Rv-Bp z34Gtrd&YUuOSq?dCOI8SS(BZ3@kV7CUN@nJ{(P4FdS~kCD^feO&ypWQZ`P~ zb1s#nTWR2)mJ^nc)2^!dQuc@vb52jVHFjkYYfufXuLjQHDq?zEj?Q_CrXn5@(9i8^j8<{?OXnNjW5o!fTnR`U#f=g= zx!2i3m->gPODe9>B07iLpvcEXD$GnvP6t&~$s@Dt9a<@B2nh47XjXC-%v5B1N8!}L zMAXcMyf_5S))7R=Xs^p-VcN*6lgo}BtR4BeF&`Dz3}eWbq$S(~ea^a`4-YZ$&dzvs z(%^FN6IkOSXP_41cT$Vgs2SFpz$qj7#^$Qlh zb&S_5m2@ys(@HY&5w(*BMgt1Haz#KqQR-J6jO7C!FKxVqEVxOXK7*OT0h_Q3Z-J1C z|7$CE2L7I+zW#l-WE!p;~Jf`tE%m0I@-g-cerIQ zx}5zx$rtUKcZ5)Z#?@-D&noaZvzqML=!l1+kzC4LldX11iVZn^=Y`lQbIu?7J; zFkBiUp{FieL`9o6yIEe?X{-h^R}g*lso-sciYlh^m}}WaVT9e^YLf`1dS1zgR-$f4 zk*Dj4RS z6;xZ3m-V|dgn_#;?ddt2Q=B&naj1ghV6TVBJv zgrBLRVO!BLPGrYWX2@hox#R!XhHh8i-_ z7%~cG`+iG%Zj$BK!IC6Hx)BV)!czSc<=0Uzx(Cgfe5*VDV>xBEnIVIa1uGm2mFuQr z7gZ+d^Vdeu zMf0gM;ClfMO!t#8M}qC#n&0a>Q`4~ zAkfxrRpca{NrSil6I$+ayKK^DZHHY&f7_KjMwpEQqNY5NANr&>2>pCFmbZsYO-$Je zCvKRMJf=J+7KUJbOk17PM!S{Wq7Ao#&Ygjm2L6#UY@iW^-0RkZ%zc<>o8HSa3e)b{ zy{Wgj{M64X1&LAkl|iM%%0-fLmh3tY<=*VMyQ_!tP9vnJLp~BFpsQE65*E0&!G0)y zqSLRd^Y{>DcPUQ2&9@S0oVI=#bnDU}J8ZG)zy&o&>CS1WXx<>Q4aGxzWc$SD_t`@< zr|ODaEeMS+yRUV%c0tSUY1r~UuE({!^A8ca_(i%#fKUl5uW=IhQ_OVmY=t&8=3FZ zYQ3LgMS>iMI|qrm;xlEL>C%eVEJLi3oU=nS(jKCF7(DqtVp0x@hl!P&IXBPn;YRwt z3;e0uG9nnSoO(VvRCb^PcF!Btx&LDNA!@bcY}(PL*mTo(!)Y<*-k1WPCLoBqTqYe} ziFind2v~n=>FmJjm4}$ZU)gF;pTxX_z_2bpgXXFr3kEd8M4C1x_qrmN5# ztUj=dYQJ1N8`-4sj73V{xLeO$iZvVo1LjlUQ zn&H4^eMAzsgce$dqBT zcQb2(SG+QK*7C(+eea!#|zO)u7agF0ZcTf_Ew{U6!x*ZI1_}2`*`Tj`cqf zp3y)!Jx&g7-|B@%_8% z_bnNR^8MW+AzV;oM3UeF6%@}hBBD&LGp&FG3XBF1dPuOM2a!m1%(PV=ka6RJX~O3o zlRH}GC&Ida)6-Dv`jH6LJF7kOr}XjK6yJd74{>h75hMtbgkE4I7NUdiaE zU;Bpdy1u=Sc|VWEs35MGVuRBjrs&4g$sUUSG$_V@m`UnkX9hmC2#!Qip@>MRF>JV) z73DmS{&{=g)ZQpqTXAjZzvXC%Cu$|+Un=$IoXd*2(1O*aC!X3ufx{Z_4j~Jo+g1s5 zvfXZkiSA^}of*rq?m1pm`rvF*>DO`8;Hf4;bLSo>CWB|^W&B-UY>NA(y?Gk4bv*Vq82q18H z`XLQu9@Fk@t1q&$h`1@iO%Ro_$OUnDX@j?iR?0u1Vh4Ghy zoNZ=lTnX-eIrYQ|Yqpo-;3kq#LN{kFsfG)VAIh#%b1qjO;2ys3UA4Dk5?A4S$*^&S zmqg31&wn4&-araXlbp7P4sH(j_Qq{$Mv0gcA=D0|F z`f%`m$&0(FSM|=8d;c@sNGczB%$(&)&pC#(@#z90HF+%WWXY@v^|>Zy!R1_~s@z6(Gj7!4~4| zFwBy=2WQ3;7Ch#{YIUmH8n)W0KxlvhiOLd*zv_C`M|4euFfjwi-otspg@j|B210Y@ z6B5uE5Y_hf_%_RLnxr-)}YIxT=a_(giOyzq7M)NAZ{U>xnmO0O$wzV1#^E5mXL+zlC4iC z{q1b6V3zH!Ln^TNB_gzvM35k;pOkBs*J}l8qL|ryzbX5DjP^7U`L>e8a6c)$+aiq* zY@L0@&9Z8~JK2hh)6&-zf?Q{4DSR2YgjQ9Oj7G8iKn09wc>qt?vW!d-ycD90I9f0$ z6xh^cNQV=MO+1O!BrXe0AXLgHL6V~Bgsxw2tb7qPzT4MX8i4_?jaAEm=0%~T{%dv1 zoXG565vO2w-+4og#UttMzj$e~CM^G&(}OP@Ay4GUSC0DK^3 z_w9KSN&WGIOI_n`trxElkjiu)9FqtHl-GrtVwY%OZq82XqGBjK8AC`eYvVX9e=8U7+T&k_7?Q>cwbxw+^JM3Mg3?fk{oub zY#getm7%^29Ss@>ppcxPrIG_3VWpQ^;IQ0XEz?+N@}|nqgA$BlM$$rmJrA3)C|}_9 zO-ET+$i}w~9?*DUt15+SL_NSL4Wo`blv5}5u>C%C2h2rEkO$Fjx&JP*ggF=O<5wv4 zX-oWjd=^>&j3p%|up*M7F-PY24=i4Y`z3w7ssN2JH*8YhbRq;aD;7Bfnc}VtE>f<; z0wa|g@tCIOcK3R(L(9AC*m%F`BDZrqcnPUGK%WV)6vVr8}L}@D6 zT6i&lPdKOcux2OrF2<wPmah8C<<*FT;e?=v6(e*bK zDITy=OAzmLETdTvsU2Rgo&KvB$^ab2(RVUD1IA&3Unf9e11Wnq9)Vc!qL`Fe2lWu6}Uat!X-@L=Pbqe0q8`SGH&VQ zv6N1xp|&|DxF)zlpGZ~RL&T-~uR2t8>6i>scQCJh0eoObb8!SJIWkX(3BIG}hXnTx z+xaxNwJENgn7kuFO2+nzOud)TK~LmmlS!~x;dNihn5qf0%!YCAM1fv_mNaBHblx5K zWh)gQ%uZgsunj|#H*5){gcl6P?k%YGA-H^K83OmWQx^vLLlfjP8%WdR5Vs4} zb%JKg8;ppMRe}fAo9QyJ30Ab`SnupS3L-E>TKBh_Ne<-Z?gJKQ7$JTMCo zv63@Sf6K>;epfn-d^F0hI zCJNX9MA{IzqM)`U>1>*?pm%w3wHAJHQ)1KV5aovVi;}v_9~-!cdE}bAg%^QUHD>fh zFpxX!V?iTl7crr*$vtPIJ?fwwbNb%y@OYx)MZ3DgYB+EK0D8@=EI9fO)Br7v+S$Cn6*#GO8&6_=WnWiOQvhEMTi$nOi7 zV5x;+h(LPFLLAQgmcLliRZ$|k0F81(et}acX1^vrUq3(z!&kQC=D3OBhlKdsNi0e> z1Zj5+LQKgxcH&ps6C>j1Wt=Ooy;GC``g7R_ayRfY?- z>m7&+>jh1fjA8oJWISR@zvQZ{aEX;CW?ms#Px{LD^8S$meQaj3BFb?vq=W9~z3YJ7 z1+oqlenqoxX0alYIuwMQ_OjQiCW*)OCWfg6{QT;`mvdy0pGJPJVYY<>*Qr!(EnW&e z8KFWzQOHZ)P7Ah<=bnxR4P=m#qTeCK#-WLkt7%y1#gS1K`}rA^`y zd(SDCXF4=&t@{9>8O4MOAO^TC%Ai`GA}TokjD4P#c(`<&Ct zb+Q)j$lec)#v|Ghgw|K#%I@%TBCq7pq0(_nfkw`ieYBQh&1l^>RM^fo9!pZ81k`ajW{vk|#O4iW(Z@)0KCKF6Qq6jRIem*we1x2f;>Tw^SC%2rk5zC%(mIj$#dL ziZ{$rvEhTPt?6%vyoc%_hQ{JL*;^*JQ2-ESl$KesZbmk8rMc2jkMuI8601^7!S6r!9mDnv{A{eZoRQB6G&*Sf-IXEszChXdzODGAF;A zrwyTV;R(n(4Tt6SVRfoINTHf3Oczs($_t|r+aN=HVN7Dq>w;1A&+-%OJqnmS_qg zUEU&buos1Nq}DHml^WH|4<25Pf{EYp z&k6*%lg%&XYpI!THdK&P6B=}Ijs!8ii9nMF!=AGK^n{l00yoU4E!0x*2ew~$Dd-W6 z^wJsv5ggTpsq3Hjg`jcrh5B^v8CgT{A9(7o!AO*S9K})v%&PpaeK^o3$;=8`Sc|9{ zlmz4tQ*Gm1D`GPT`<7iU?GSf{L^65FvfaRsQi$E7RoUocDYAJV8Sq(aQVL0<=Gl%$ zW+8RV)!))ppANO3qd&=D2$~HQe2dp&Q%c|P@vqML;M;}jRb9ji;quA2ZqK9l-_Ep) zzoO_ew^NSC$x!bn%e%k}!tj>om^i=TCN&PYLwDbOqj@mVRR8B5q zzRyDgj$lenH#GlbHi~M#5o3P)cVlBNzUutGBO99~US*H6@ZBg)T@XrpA`cZXPc)$V zY22a|a7G%CF0MR4wosx-T-Bs z%%@|H|& zy}qm>o?}W$K$Csp-Iu1>O7tJ-#b7mF+%_uVAHrf-U}AtIubsL`*+IS;4|~dhS8IPx z;JfF{_=qHhz-}OMjmTZe?grlB{ctrA|K-wSEW;)Is5_F?2~1@F~#l7IaE7bui%O6!5e4Cus8~MaUbBMQqR&yL8b}S zK*r}=(bp}@vrXUOBMV{p+H6i)|5p71siDno^t@*>db!7v<8V3eg!h_;jtXw`R8Ze4 zi4}s;!>Wlfv#d^$kRh?`2xO9mT_+0LJZ_VJ{^3q%=|U z0}2=9rw>aiviKQY1ufc!g^lURxplPG1#|O@BOWAEdn6Yy8KZMT5YgbGRNe5mC01@Aauy?`YR2Avvp-^u1M%Q7|J5r~@Q!46 z&|*EQB;C28=Qn3fFg3LX0h~h!Z|@a|+K5PFLHGeZ9#PwnpzNj`Z2!nkj_dRC72bAd z_uF-ofRffBl`{v?*R?qq;6Q2r%rHi7gMa4t!z^p zM`LWL*+e6+#}6=`0d`Y@4-dQ4{eZ3Ke0sa(y=&hrqxR|WcY387EOPSWO|**h>p~pf z{M0Ix?~P)g>R|a@3%1cP9ypXQ^ut-3W_=1JhMaywr(MS;A9p^Iy~)CzM4sbjBO0$GyA; zWIv1BeeLN*IudfCTFK4Y%b;EEm=fP}HVH3434&66zxTrqBdvg_KZ#9?c0vkG-VT^b zr8hZwzRA>C;&>QrtcZOtMwmY6-W6wG^Rew>WMRYo7Q?$YJJ0u?`Be;mGvSAzWHZx4 zv*kp6fSwMkb$`|L@$u*PZ8voNx|LOWx~AWi)dR}FAW;^u!?Je)?MkX8G|*I>$|%wE zaL!``G12)1p6VCv3fPg9$~ew5V(h@Ybv~B2#BWyA%f7VDXUlBly!#l6>7X+|)tfME zGanMrsxBkyxgu)o@bT`RvrMq+C2`YN^NVKnHD#B#w2{TOsu80Q$=vO=kgNV9)TQvtb;Z^0yJ*i0 zz-!hgC@wOT>r$2FTw5D`A)(sV$#3Z;ilXgLA)e3tlq2$RJuhrzXUCzlx@qGsTI-&B z%@*te6*1!-P0HGLsUa+{lzAH}Ki&JkG?$ZH9biWzTSm_)G0vU3P_SRLY!pUalNI;d zw(Fp84zGkk4f_uf|7j5r;GXOzTy>x&&FdUzikL5~`Yw_}oR--(ojsKv4Q?%lmhLa4za z<&>)!Z`TjWE0~otN|?WlCb|u2J*BGcPX{%2K^uE}Zq;|9cc$iZ*7dZ&l&NfAkq$ii zdnjTTmvrkJu4A6=bIbV~zOH~}(*q#!Yz2MCOZ{JmmE(hUa87`Q)<*pIb@9pW#k$i7 z`5ltC8?_6d&8~)Pk4$GN+VfQ2&#_?qr}dqBf`)<98Y&VAY7jkxEmtq9qRJL=WVC8K zD+Yx2+!j`i&(mlbv9hGeC&~)(VAk(vZ_`c{GVEpLoPPpMMnVkaP|j5yw(NR3pAGr) uzL~7!Ps+ni-|Lav6-AEtWXPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91TA%{}1ONa40RR91MF0Q*0IST4RCodHoL_7dMI6U_*Fx!k zC|I;8SVMve0VP32P8G41I!5o*?(Xg_Kt4&E z#}l%URUx~&y1L#~!W|~$h;D0ZtEsB0dWE0t7CLlIrwKPezcd42M?*uyJI=i89yE=v z=tZv3G~5DCBb@hJKcoi?~3ZSK~v0xN_yn(quCED)#G;dKI*xFaZYQk1YLG z%W_oE{AA}04Gn#Y-BX3#T8jVz4>N@v4P14wCS3>QQxS4=&;+hBea!Go9YB5?7lLdU-;51rtV_f zL%Ev}wMWlzN)@K)-PuD$cCk@#@79WnX8PK;l6k!fK z89kT77#3ku)Cl5L)|n#eDp&;2C0p`pj*fbh5;zC8x3|~T)YN1yYK+cwb#-+cjnzI0 zPar?RJUEm5B$8mC#6?cvH>`k`k@qA8^w3{zYw#3yDX#vJcHDuE6 znEQ!Fp1W75P4uIk__yo>whh>-UzNx#ceya>1de(zi~{>IKs=Sv$n`XA559ErOgI6! z<;~5_eMX_Ux3{;MS5q3j^Bnq<@LI#Wwm*%Iv`)r*q_-KJU(AS}1DFfWJo=@T!$G9Z zp>N}@$Ai4}Xm#kysAnqm96_ftCj0~RjSPUpMxmH~zCb@CPgRA)F){5WYRWMwZzG;(qQpD-BleSM!Z3OJywF0M|}#UP%UOZWJy>!(jhNO?-4lT7IgxbM*On3F`DM{rZOe)rDCenHPW~D7VB_9`HY8$> zE$Em58}}BpArW(ILB|Z(xVNAUiI`&xI%dGey#;MZ#2j1DF#|U4Eoeg`=GcOc8L)A0 zK^qb=#};(VfQ@^B)~gvC;w3O5(1F_r;;s9J@}3sx;Tx*Uwh8kdAq9EKDK~7{Rv9w zfR9#99p^%q$QrVBYZtI*D2bCwHVA&sVbHG6t!xauKU=hnhUx?U^(O6#S77_5(Q!N< z|L>dhDDj81=MLp#uZ^2qIn*a3p1?_7OMA61FBA074d}L;v@4#4?I-xmz{an3rhe(N z36nghMy*D$s19mgTeqACxA8^ zFmr>99QrfZ^wpqmy(4BWvl5ME} za&WXbAGTBo0!PJIbF}8QV&&1B@QG&lqLXhy(Sd5T#@6aq(> zGFCI1|1*VhBU%b&@Xd+V)yIv2jUy8{E{0CX@{l*8jY~%MG)DG*($5&3jYAPQz)05T zah|WX@ZG&%F^apsPC zFVB8<>dW8JcoYO}3hP42?YjPebrm}O39FuB%|LFXppknTIR=j--0>ZPNA7|*8i>%K z{hV}kbTsgYQX{^Wvsy!?Q@9Y66KhJ6jLE-WoiXe5Tp|s`G>>P8i3jOhFMUh#=Ux}} d4-CcD|3CQ&WO^l{8{Yr`002ovPDHLkV1l7$@DKn1 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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( From 1ea0fd3c5092733d90af6f84cc7867d032d6078e Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Fri, 19 Dec 2025 20:38:00 +0000 Subject: [PATCH 06/10] Rename --- .../xcschemes/xcschememanagement.plist | 10 +-- .../MacMagazine.xcodeproj/project.pbxproj | 80 +++++++++--------- .../xcschemes/xcschememanagement.plist | 9 +- .../AppIntent.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../WidgetBackground.colorset/Contents.json | 0 .../logo_color.imageset/Contents.json | 0 .../logo_color.imageset/logo@3x 3.png | Bin .../logo_white.imageset/Contents.json | 0 .../logo_white.imageset/logo.png | Bin .../{WidgetWatch => WatchWidget}/Info.plist | 0 .../WatchWidget.entitlements} | 0 .../WatchWidget.swift} | 17 ++-- .../WatchWidgetBundle.swift} | 4 +- 16 files changed, 61 insertions(+), 59 deletions(-) rename MacMagazine/{WidgetWatch => WatchWidget}/AppIntent.swift (100%) rename MacMagazine/{WidgetWatch => WatchWidget}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename MacMagazine/{WidgetWatch => WatchWidget}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename MacMagazine/{WidgetWatch => WatchWidget}/Assets.xcassets/Contents.json (100%) rename MacMagazine/{WidgetWatch => WatchWidget}/Assets.xcassets/WidgetBackground.colorset/Contents.json (100%) rename MacMagazine/{WidgetWatch => WatchWidget}/Assets.xcassets/logo_color.imageset/Contents.json (100%) rename MacMagazine/{WidgetWatch => WatchWidget}/Assets.xcassets/logo_color.imageset/logo@3x 3.png (100%) rename MacMagazine/{WidgetWatch => WatchWidget}/Assets.xcassets/logo_white.imageset/Contents.json (100%) rename MacMagazine/{WidgetWatch => WatchWidget}/Assets.xcassets/logo_white.imageset/logo.png (100%) rename MacMagazine/{WidgetWatch => WatchWidget}/Info.plist (100%) rename MacMagazine/{WidgetWatchExtension.entitlements => WatchWidget/WatchWidget.entitlements} (100%) rename MacMagazine/{WidgetWatch/WidgetWatch.swift => WatchWidget/WatchWidget.swift} (97%) rename MacMagazine/{WidgetWatch/WidgetWatchBundle.swift => WatchWidget/WatchWidgetBundle.swift} (54%) diff --git a/MacMagazine/Features/FeedLibrary/.swiftpm/xcode/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist b/MacMagazine/Features/FeedLibrary/.swiftpm/xcode/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist index d3090d19..a1936be5 100644 --- a/MacMagazine/Features/FeedLibrary/.swiftpm/xcode/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/MacMagazine/Features/FeedLibrary/.swiftpm/xcode/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -4,11 +4,6 @@ SchemeUserState - FeedLibraryTests.xcscheme_^#shared#^_ - - orderHint - 5 - News-Package.xcscheme_^#shared#^_ orderHint @@ -22,6 +17,11 @@ primary + FeedLibraryTests + + primary + + News primary diff --git a/MacMagazine/MacMagazine.xcodeproj/project.pbxproj b/MacMagazine/MacMagazine.xcodeproj/project.pbxproj index e6b0ab71..aef6ec6d 100644 --- a/MacMagazine/MacMagazine.xcodeproj/project.pbxproj +++ b/MacMagazine/MacMagazine.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* 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, ); }; }; + 88C8B2F72EF476E200044DE9 /* WatchWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 88C8B2E62EF476E100044DE9 /* WatchWidget.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 */; }; @@ -23,7 +23,7 @@ 9BCD94C62E9971C0007C44B6 /* MacMagazineLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 9BCD94C52E9971C0007C44B6 /* MacMagazineLibrary */; }; 9BDD424F2EE9E82F00A9BD85 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9BDD424E2EE9E82F00A9BD85 /* WidgetKit.framework */; }; 9BDD42512EE9E82F00A9BD85 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9BDD42502EE9E82F00A9BD85 /* SwiftUI.framework */; }; - 9BDD42602EE9E83100A9BD85 /* WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9BDD424D2EE9E82F00A9BD85 /* WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 9BDD42602EE9E83100A9BD85 /* Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9BDD424D2EE9E82F00A9BD85 /* Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,7 +57,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 88C8B2F72EF476E200044DE9 /* WidgetWatchExtension.appex in Embed Foundation Extensions */, + 88C8B2F72EF476E200044DE9 /* WatchWidget.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -79,7 +79,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 9BDD42602EE9E83100A9BD85 /* WidgetExtension.appex in Embed Foundation Extensions */, + 9BDD42602EE9E83100A9BD85 /* Widget.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -87,8 +87,7 @@ /* 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 = ""; }; + 88C8B2E62EF476E100044DE9 /* WatchWidget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WatchWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; @@ -99,25 +98,25 @@ 9BA8288C2EEC7AAA00F26FCB /* WatchApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WatchApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9BB3888E2EE0B2E4007AC71C /* MacMagazine.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MacMagazine.xctestplan; sourceTree = ""; }; 9BCD94C42E997032007C44B6 /* MacMagazineLibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MacMagazineLibrary; sourceTree = ""; }; - 9BDD424D2EE9E82F00A9BD85 /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 9BDD424D2EE9E82F00A9BD85 /* Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Widget.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 9BDD424E2EE9E82F00A9BD85 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 9BDD42502EE9E82F00A9BD85 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 88C8B2FB2EF476E200044DE9 /* Exceptions for "WidgetWatch" folder in "WidgetWatchExtension" target */ = { + 88C8B2FB2EF476E200044DE9 /* Exceptions for "WatchWidget" folder in "WatchWidget" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, ); - target = 88C8B2E52EF476E100044DE9 /* WidgetWatchExtension */; + target = 88C8B2E52EF476E100044DE9 /* WatchWidget */; }; - 88C8B3052EF577EE00044DE9 /* Exceptions for "WatchApp" folder in "WidgetWatchExtension" target */ = { + 88C8B3052EF577EE00044DE9 /* Exceptions for "WatchApp" folder in "WatchWidget" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( WatchWidgetShared.swift, ); - target = 88C8B2E52EF476E100044DE9 /* WidgetWatchExtension */; + target = 88C8B2E52EF476E100044DE9 /* WatchWidget */; }; 9B9E9DD12E986F8500255820 /* Exceptions for "MacMagazine" folder in "MacMagazine" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; @@ -133,22 +132,22 @@ ); target = 9BA8288B2EEC7AAA00F26FCB /* WatchApp */; }; - 9BDD42652EE9E83100A9BD85 /* Exceptions for "Widget" folder in "WidgetExtension" target */ = { + 9BDD42652EE9E83100A9BD85 /* Exceptions for "Widget" folder in "Widget" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, ); - target = 9BDD424C2EE9E82F00A9BD85 /* WidgetExtension */; + target = 9BDD424C2EE9E82F00A9BD85 /* Widget */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 88C8B2E92EF476E100044DE9 /* WidgetWatch */ = { + 88C8B2E92EF476E100044DE9 /* WatchWidget */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( - 88C8B2FB2EF476E200044DE9 /* Exceptions for "WidgetWatch" folder in "WidgetWatchExtension" target */, + 88C8B2FB2EF476E200044DE9 /* Exceptions for "WatchWidget" folder in "WatchWidget" target */, ); - path = WidgetWatch; + path = WatchWidget; sourceTree = ""; }; 9B9E9DC62E986F8400255820 /* MacMagazine */ = { @@ -163,7 +162,7 @@ 9BA8288D2EEC7AAA00F26FCB /* WatchApp */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( - 88C8B3052EF577EE00044DE9 /* Exceptions for "WatchApp" folder in "WidgetWatchExtension" target */, + 88C8B3052EF577EE00044DE9 /* Exceptions for "WatchApp" folder in "WatchWidget" target */, ); path = WatchApp; sourceTree = ""; @@ -171,7 +170,7 @@ 9BDD42522EE9E82F00A9BD85 /* Widget */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( - 9BDD42652EE9E83100A9BD85 /* Exceptions for "Widget" folder in "WidgetExtension" target */, + 9BDD42652EE9E83100A9BD85 /* Exceptions for "Widget" folder in "Widget" target */, ); path = Widget; sourceTree = ""; @@ -227,13 +226,12 @@ 9B9E9DBB2E986F8400255820 = { isa = PBXGroup; children = ( - 88C8B3012EF4AE7400044DE9 /* WidgetWatchExtension.entitlements */, 9BB3888E2EE0B2E4007AC71C /* MacMagazine.xctestplan */, 9B9E9DDA2E98701C00255820 /* Features */, 9B9E9DC62E986F8400255820 /* MacMagazine */, 9BDD42522EE9E82F00A9BD85 /* Widget */, 9BA8288D2EEC7AAA00F26FCB /* WatchApp */, - 88C8B2E92EF476E100044DE9 /* WidgetWatch */, + 88C8B2E92EF476E100044DE9 /* WatchWidget */, 9B9E9DDE2E98709000255820 /* Frameworks */, 9B9E9DC52E986F8400255820 /* Products */, ); @@ -243,9 +241,9 @@ isa = PBXGroup; children = ( 9B9E9DC42E986F8400255820 /* MacMagazine.app */, - 9BDD424D2EE9E82F00A9BD85 /* WidgetExtension.appex */, + 9BDD424D2EE9E82F00A9BD85 /* Widget.appex */, 9BA8288C2EEC7AAA00F26FCB /* WatchApp.app */, - 88C8B2E62EF476E100044DE9 /* WidgetWatchExtension.appex */, + 88C8B2E62EF476E100044DE9 /* WatchWidget.appex */, ); name = Products; sourceTree = ""; @@ -276,9 +274,9 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 88C8B2E52EF476E100044DE9 /* WidgetWatchExtension */ = { + 88C8B2E52EF476E100044DE9 /* WatchWidget */ = { isa = PBXNativeTarget; - buildConfigurationList = 88C8B2F82EF476E200044DE9 /* Build configuration list for PBXNativeTarget "WidgetWatchExtension" */; + buildConfigurationList = 88C8B2F82EF476E200044DE9 /* Build configuration list for PBXNativeTarget "WatchWidget" */; buildPhases = ( 88C8B2E22EF476E100044DE9 /* Sources */, 88C8B2E32EF476E100044DE9 /* Frameworks */, @@ -289,14 +287,14 @@ dependencies = ( ); fileSystemSynchronizedGroups = ( - 88C8B2E92EF476E100044DE9 /* WidgetWatch */, + 88C8B2E92EF476E100044DE9 /* WatchWidget */, ); - name = WidgetWatchExtension; + name = WatchWidget; packageProductDependencies = ( 88C8B2FD2EF48DC500044DE9 /* FeedLibrary */, ); productName = WidgetWatchExtension; - productReference = 88C8B2E62EF476E100044DE9 /* WidgetWatchExtension.appex */; + productReference = 88C8B2E62EF476E100044DE9 /* WatchWidget.appex */; productType = "com.apple.product-type.app-extension"; }; 9B9E9DC32E986F8400255820 /* MacMagazine */ = { @@ -357,9 +355,9 @@ productReference = 9BA8288C2EEC7AAA00F26FCB /* WatchApp.app */; productType = "com.apple.product-type.application"; }; - 9BDD424C2EE9E82F00A9BD85 /* WidgetExtension */ = { + 9BDD424C2EE9E82F00A9BD85 /* Widget */ = { isa = PBXNativeTarget; - buildConfigurationList = 9BDD42642EE9E83100A9BD85 /* Build configuration list for PBXNativeTarget "WidgetExtension" */; + buildConfigurationList = 9BDD42642EE9E83100A9BD85 /* Build configuration list for PBXNativeTarget "Widget" */; buildPhases = ( 9BDD42492EE9E82F00A9BD85 /* Sources */, 9BDD424A2EE9E82F00A9BD85 /* Frameworks */, @@ -372,13 +370,13 @@ fileSystemSynchronizedGroups = ( 9BDD42522EE9E82F00A9BD85 /* Widget */, ); - name = WidgetExtension; + name = Widget; packageProductDependencies = ( 9B4F666E2EE9F223008ACACB /* FeedLibrary */, 9B5143C42EF4CE9800ED4F39 /* MacMagazineLibrary */, ); productName = WidgetExtension; - productReference = 9BDD424D2EE9E82F00A9BD85 /* WidgetExtension.appex */; + productReference = 9BDD424D2EE9E82F00A9BD85 /* Widget.appex */; productType = "com.apple.product-type.app-extension"; }; /* End PBXNativeTarget section */ @@ -422,9 +420,9 @@ projectRoot = ""; targets = ( 9B9E9DC32E986F8400255820 /* MacMagazine */, - 9BDD424C2EE9E82F00A9BD85 /* WidgetExtension */, + 9BDD424C2EE9E82F00A9BD85 /* Widget */, 9BA8288B2EEC7AAA00F26FCB /* WatchApp */, - 88C8B2E52EF476E100044DE9 /* WidgetWatchExtension */, + 88C8B2E52EF476E100044DE9 /* WatchWidget */, ); }; /* End PBXProject section */ @@ -516,7 +514,7 @@ /* Begin PBXTargetDependency section */ 88C8B2F62EF476E200044DE9 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 88C8B2E52EF476E100044DE9 /* WidgetWatchExtension */; + target = 88C8B2E52EF476E100044DE9 /* WatchWidget */; targetProxy = 88C8B2F52EF476E200044DE9 /* PBXContainerItemProxy */; }; 9BA828AB2EEC7AAB00F26FCB /* PBXTargetDependency */ = { @@ -526,7 +524,7 @@ }; 9BDD425F2EE9E83100A9BD85 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 9BDD424C2EE9E82F00A9BD85 /* WidgetExtension */; + target = 9BDD424C2EE9E82F00A9BD85 /* Widget */; targetProxy = 9BDD425E2EE9E83100A9BD85 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -537,12 +535,12 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = WidgetWatchExtension.entitlements; + CODE_SIGN_ENTITLEMENTS = WatchWidget/WatchWidget.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 5.1.0; DEVELOPMENT_TEAM = A5VW9QUF9L; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = WidgetWatch/Info.plist; + INFOPLIST_FILE = WatchWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MacMagazine; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( @@ -571,12 +569,12 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = WidgetWatchExtension.entitlements; + CODE_SIGN_ENTITLEMENTS = WatchWidget/WatchWidget.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 5.1.0; DEVELOPMENT_TEAM = A5VW9QUF9L; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = WidgetWatch/Info.plist; + INFOPLIST_FILE = WatchWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MacMagazine; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( @@ -932,7 +930,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 88C8B2F82EF476E200044DE9 /* Build configuration list for PBXNativeTarget "WidgetWatchExtension" */ = { + 88C8B2F82EF476E200044DE9 /* Build configuration list for PBXNativeTarget "WatchWidget" */ = { isa = XCConfigurationList; buildConfigurations = ( 88C8B2F92EF476E200044DE9 /* Debug */, @@ -968,7 +966,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 9BDD42642EE9E83100A9BD85 /* Build configuration list for PBXNativeTarget "WidgetExtension" */ = { + 9BDD42642EE9E83100A9BD85 /* Build configuration list for PBXNativeTarget "Widget" */ = { isa = XCConfigurationList; buildConfigurations = ( 9BDD42622EE9E83100A9BD85 /* Debug */, diff --git a/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist b/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist index e26f1a86..34afa042 100644 --- a/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -22,7 +22,7 @@ WatchApp.xcscheme_^#shared#^_ orderHint - 2 + 1 WatchkApp Watch App (Notification).xcscheme_^#shared#^_ @@ -42,7 +42,12 @@ WidgetExtension.xcscheme_^#shared#^_ orderHint - 3 + 4 + + WidgetWatchExtension.xcscheme_^#shared#^_ + + orderHint + 2 SuppressBuildableAutocreation diff --git a/MacMagazine/WidgetWatch/AppIntent.swift b/MacMagazine/WatchWidget/AppIntent.swift similarity index 100% rename from MacMagazine/WidgetWatch/AppIntent.swift rename to MacMagazine/WatchWidget/AppIntent.swift diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/AccentColor.colorset/Contents.json b/MacMagazine/WatchWidget/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from MacMagazine/WidgetWatch/Assets.xcassets/AccentColor.colorset/Contents.json rename to MacMagazine/WatchWidget/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/AppIcon.appiconset/Contents.json b/MacMagazine/WatchWidget/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from MacMagazine/WidgetWatch/Assets.xcassets/AppIcon.appiconset/Contents.json rename to MacMagazine/WatchWidget/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/Contents.json b/MacMagazine/WatchWidget/Assets.xcassets/Contents.json similarity index 100% rename from MacMagazine/WidgetWatch/Assets.xcassets/Contents.json rename to MacMagazine/WatchWidget/Assets.xcassets/Contents.json diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/WidgetBackground.colorset/Contents.json b/MacMagazine/WatchWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json similarity index 100% rename from MacMagazine/WidgetWatch/Assets.xcassets/WidgetBackground.colorset/Contents.json rename to MacMagazine/WatchWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/Contents.json b/MacMagazine/WatchWidget/Assets.xcassets/logo_color.imageset/Contents.json similarity index 100% rename from MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/Contents.json rename to MacMagazine/WatchWidget/Assets.xcassets/logo_color.imageset/Contents.json diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/logo@3x 3.png b/MacMagazine/WatchWidget/Assets.xcassets/logo_color.imageset/logo@3x 3.png similarity index 100% rename from MacMagazine/WidgetWatch/Assets.xcassets/logo_color.imageset/logo@3x 3.png rename to MacMagazine/WatchWidget/Assets.xcassets/logo_color.imageset/logo@3x 3.png diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/logo_white.imageset/Contents.json b/MacMagazine/WatchWidget/Assets.xcassets/logo_white.imageset/Contents.json similarity index 100% rename from MacMagazine/WidgetWatch/Assets.xcassets/logo_white.imageset/Contents.json rename to MacMagazine/WatchWidget/Assets.xcassets/logo_white.imageset/Contents.json diff --git a/MacMagazine/WidgetWatch/Assets.xcassets/logo_white.imageset/logo.png b/MacMagazine/WatchWidget/Assets.xcassets/logo_white.imageset/logo.png similarity index 100% rename from MacMagazine/WidgetWatch/Assets.xcassets/logo_white.imageset/logo.png rename to MacMagazine/WatchWidget/Assets.xcassets/logo_white.imageset/logo.png diff --git a/MacMagazine/WidgetWatch/Info.plist b/MacMagazine/WatchWidget/Info.plist similarity index 100% rename from MacMagazine/WidgetWatch/Info.plist rename to MacMagazine/WatchWidget/Info.plist diff --git a/MacMagazine/WidgetWatchExtension.entitlements b/MacMagazine/WatchWidget/WatchWidget.entitlements similarity index 100% rename from MacMagazine/WidgetWatchExtension.entitlements rename to MacMagazine/WatchWidget/WatchWidget.entitlements diff --git a/MacMagazine/WidgetWatch/WidgetWatch.swift b/MacMagazine/WatchWidget/WatchWidget.swift similarity index 97% rename from MacMagazine/WidgetWatch/WidgetWatch.swift rename to MacMagazine/WatchWidget/WatchWidget.swift index 43b5d933..2d85cbe1 100644 --- a/MacMagazine/WidgetWatch/WidgetWatch.swift +++ b/MacMagazine/WatchWidget/WatchWidget.swift @@ -72,7 +72,7 @@ struct Provider: AppIntentTimelineProvider { // MARK: - Entry View -struct WidgetWatchEntryView: View { +struct WatchWidgetEntryView: View { let entry: SimpleEntry @Environment(\.widgetFamily) private var family @@ -102,7 +102,7 @@ struct WidgetWatchEntryView: View { // MARK: - Layouts -private extension WidgetWatchEntryView { +private extension WatchWidgetEntryView { // ACCESSORY CIRCULAR var circular: some View { @@ -225,7 +225,7 @@ private extension WidgetWatchEntryView { // MARK: - Widget -struct WidgetWatch: Widget { +struct WatchWidget: Widget { let kind: String = "WidgetWatch" @@ -235,7 +235,7 @@ struct WidgetWatch: Widget { intent: ConfigurationAppIntent.self, provider: Provider() ) { entry in - WidgetWatchEntryView(entry: entry) + WatchWidgetEntryView(entry: entry) } .configurationDisplayName("MacMagazine") .description("Acesso rápido às notícias do MacMagazine.") @@ -251,7 +251,7 @@ struct WidgetWatch: Widget { #if DEBUG #Preview("Circular", as: .accessoryCircular) { - WidgetWatch() + WatchWidget() } timeline: { SimpleEntry( date: .now, @@ -263,7 +263,7 @@ struct WidgetWatch: Widget { } #Preview("Corner", as: .accessoryCorner) { - WidgetWatch() + WatchWidget() } timeline: { SimpleEntry( date: .now, @@ -275,7 +275,7 @@ struct WidgetWatch: Widget { } #Preview("Inline", as: .accessoryInline) { - WidgetWatch() + WatchWidget() } timeline: { SimpleEntry( date: .now, @@ -287,7 +287,7 @@ struct WidgetWatch: Widget { } #Preview("Rectangular", as: .accessoryRectangular) { - WidgetWatch() + WatchWidget() } timeline: { SimpleEntry( date: .now, @@ -297,5 +297,4 @@ struct WidgetWatch: Widget { lastPostDate: .now.addingTimeInterval(-60 * 90) ) } - #endif diff --git a/MacMagazine/WidgetWatch/WidgetWatchBundle.swift b/MacMagazine/WatchWidget/WatchWidgetBundle.swift similarity index 54% rename from MacMagazine/WidgetWatch/WidgetWatchBundle.swift rename to MacMagazine/WatchWidget/WatchWidgetBundle.swift index b120048e..151e1ce0 100644 --- a/MacMagazine/WidgetWatch/WidgetWatchBundle.swift +++ b/MacMagazine/WatchWidget/WatchWidgetBundle.swift @@ -2,8 +2,8 @@ import SwiftUI import WidgetKit @main -struct WidgetWatchBundle: WidgetBundle { +struct WatchWidgetBundle: WidgetBundle { var body: some Widget { - WidgetWatch() + WatchWidget() } } From d13940a7cb51161981e818d57a8b4bf3869e536f Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Sat, 20 Dec 2025 09:57:37 -0300 Subject: [PATCH 07/10] Refactors Watch app widget integration Migrates to a new widget architecture for the Watch app, enhancing data handling and widget updates. This includes: - Moving shared widget logic to a dedicated file. - Updating data transfer objects for widget information. - Simplifying data loading and refreshing in the main view model. - Improving accessibility support for widgets. - Corrects target for resources files. --- .../MainApp/WatchNotificationsDelegate.swift | 6 +- .../AccentColor.colorset/Contents.json | 0 .../ViewModel/FeedMainViewModel.swift | 32 +- MacMagazine/WatchApp/Views/FeedMainView.swift | 11 +- MacMagazine/WatchApp/WatchWidgetShared.swift | 34 +- .../Extensions/DateFormatter.swift | 18 ++ .../Extensions/WidgetAccessibility+View.swift | 61 ++++ .../WatchWidget/Model/WatchWidgetModel.swift | 12 + .../AccentColor.colorset}/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../logo_color.imageset/Contents.json | 0 .../logo_color.imageset/logo@3x 3.png | Bin .../logo_white.imageset/Contents.json | 0 .../logo_white.imageset/logo.png | Bin .../Views/WatchWidgetEntryView.swift | 140 ++++++++ MacMagazine/WatchWidget/WatchWidget.swift | 300 ------------------ .../WatchWidget/{ => Widget}/AppIntent.swift | 2 +- .../WatchWidget/Widget/WatchWidget.swift | 77 +++++ .../{ => Widget}/WatchWidgetBundle.swift | 0 .../Widget/WatchWidgetProvider.swift | 55 ++++ 21 files changed, 389 insertions(+), 359 deletions(-) rename MacMagazine/{WatchWidget => WatchApp/Resources}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) create mode 100644 MacMagazine/WatchWidget/Extensions/DateFormatter.swift create mode 100644 MacMagazine/WatchWidget/Extensions/WidgetAccessibility+View.swift create mode 100644 MacMagazine/WatchWidget/Model/WatchWidgetModel.swift rename MacMagazine/WatchWidget/{Assets.xcassets/WidgetBackground.colorset => Resources/Assets.xcassets/AccentColor.colorset}/Contents.json (100%) rename MacMagazine/WatchWidget/{ => Resources}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename MacMagazine/WatchWidget/{ => Resources}/Assets.xcassets/Contents.json (100%) rename MacMagazine/WatchWidget/{ => Resources}/Assets.xcassets/logo_color.imageset/Contents.json (100%) rename MacMagazine/WatchWidget/{ => Resources}/Assets.xcassets/logo_color.imageset/logo@3x 3.png (100%) rename MacMagazine/WatchWidget/{ => Resources}/Assets.xcassets/logo_white.imageset/Contents.json (100%) rename MacMagazine/WatchWidget/{ => Resources}/Assets.xcassets/logo_white.imageset/logo.png (100%) create mode 100644 MacMagazine/WatchWidget/Views/WatchWidgetEntryView.swift delete mode 100644 MacMagazine/WatchWidget/WatchWidget.swift rename MacMagazine/WatchWidget/{ => Widget}/AppIntent.swift (76%) create mode 100644 MacMagazine/WatchWidget/Widget/WatchWidget.swift rename MacMagazine/WatchWidget/{ => Widget}/WatchWidgetBundle.swift (100%) create mode 100644 MacMagazine/WatchWidget/Widget/WatchWidgetProvider.swift diff --git a/MacMagazine/WatchApp/MainApp/WatchNotificationsDelegate.swift b/MacMagazine/WatchApp/MainApp/WatchNotificationsDelegate.swift index a1cde677..3710742a 100644 --- a/MacMagazine/WatchApp/MainApp/WatchNotificationsDelegate.swift +++ b/MacMagazine/WatchApp/MainApp/WatchNotificationsDelegate.swift @@ -42,14 +42,14 @@ final class WatchNotificationsDelegate: NSObject, }() MacMagazineWidgetSharedStore.write( - snapshot: .init( - postId: postId, + post: .init( + id: postId, title: title, date: date ) ) // Atualiza as complicações - WidgetCenter.shared.reloadTimelines(ofKind: "WidgetWatch") + WidgetCenter.shared.reloadTimelines(ofKind: "WatchWidget") } } diff --git a/MacMagazine/WatchWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/MacMagazine/WatchApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from MacMagazine/WatchWidget/Assets.xcassets/AccentColor.colorset/Contents.json rename to MacMagazine/WatchApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift b/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift index 57189732..6f70fcdc 100644 --- a/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift +++ b/MacMagazine/WatchApp/ViewModel/FeedMainViewModel.swift @@ -13,7 +13,6 @@ final class FeedMainViewModel { private(set) var status: FeedViewModel.Status = .loading var selectedIndex: Int = 0 - var showActions: Bool = false var showContextMenu: Bool = false var selectedPostForDetail: FeedDB? private(set) var isRefreshing: Bool = false @@ -21,9 +20,6 @@ final class FeedMainViewModel { // MARK: - Private private let feedViewModel: FeedViewModel - private var didLoadInitial: Bool = false - private var lastRefreshAt: Date? - private let staleInterval: TimeInterval = 30 * 60 // MARK: - Init @@ -34,26 +30,6 @@ final class FeedMainViewModel { // MARK: - Public API - func loadInitialIfNeeded(hasItems: Bool, modelContext: ModelContext) async { - guard !didLoadInitial else { return } - didLoadInitial = true - - if hasItems { - status = .done - persistLatestPostSnapshot(from: modelContext) - } - - let shouldAutoRefresh: Bool = { - if !hasItems { return true } - guard let last = lastRefreshAt else { return true } - return Date().timeIntervalSince(last) > staleInterval - }() - - if shouldAutoRefresh { - await refresh(modelContext: modelContext) - } - } - func refresh(modelContext: ModelContext) async { guard !isRefreshing else { return } isRefreshing = true @@ -61,17 +37,15 @@ final class FeedMainViewModel { _ = try? await feedViewModel.getWatchFeed() status = feedViewModel.status - lastRefreshAt = Date() persistLatestPostSnapshot(from: modelContext) - WidgetCenter.shared.reloadTimelines(ofKind: "WidgetWatch") + WidgetCenter.shared.reloadTimelines(ofKind: "WatchWidget") } func toggleFavorite(post: FeedDB, modelContext: ModelContext) { post.favorite.toggle() try? modelContext.save() - showActions = true } // MARK: - Snapshot para Widget @@ -85,8 +59,8 @@ final class FeedMainViewModel { guard let last = try? modelContext.fetch(descriptor).first else { return } MacMagazineWidgetSharedStore.write( - snapshot: .init( - postId: last.postId, + post: .init( + id: last.postId, title: last.title, date: last.pubDate ) diff --git a/MacMagazine/WatchApp/Views/FeedMainView.swift b/MacMagazine/WatchApp/Views/FeedMainView.swift index c525856b..4f81c1b4 100644 --- a/MacMagazine/WatchApp/Views/FeedMainView.swift +++ b/MacMagazine/WatchApp/Views/FeedMainView.swift @@ -33,17 +33,13 @@ struct FeedMainView: View { rootContent .navigationBarTitleDisplayMode(.inline) .task { - await viewModel.loadInitialIfNeeded( - hasItems: !items.isEmpty, - modelContext: modelContext - ) + await viewModel.refresh(modelContext: modelContext) } .navigationDestination(item: $viewModel.selectedPostForDetail) { post in FeedDetailView(viewModel: viewModel, post: post) } .onOpenURL { url in - guard url.scheme == "macmagazine" else { return } - guard url.host == "news" else { return } + guard url.scheme == "macmagazine", url.host == "news" else { return } let parts = url.pathComponents.filter { $0 != "/" } if parts.count >= 2, parts[0] == "post" { @@ -52,9 +48,6 @@ struct FeedMainView: View { } } } - .task { - await viewModel.refresh(modelContext: modelContext) - } } // MARK: - Root Content diff --git a/MacMagazine/WatchApp/WatchWidgetShared.swift b/MacMagazine/WatchApp/WatchWidgetShared.swift index 33ed17de..59d88167 100644 --- a/MacMagazine/WatchApp/WatchWidgetShared.swift +++ b/MacMagazine/WatchApp/WatchWidgetShared.swift @@ -1,42 +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" + static let postId = "macmagazine.widget.post.id" + static let postTitle = "macmagazine.widget.post.title" + static let postDate = "macmagazine.widget.post.date" } - struct Snapshot: Equatable { - let postId: String + struct LastPost: Equatable { + let id: String let title: String let date: Date } - static func write(snapshot: Snapshot) { + static func write(post: LastPost) { 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() + defaults.set(post.id, forKey: Keys.postId) + defaults.set(post.title, forKey: Keys.postTitle) + defaults.set(post.date, forKey: Keys.postDate) } - static func readSnapshot() -> Snapshot? { + static func readPost() -> LastPost? { 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 + let id = defaults.string(forKey: Keys.postId), + let title = defaults.string(forKey: Keys.postTitle), + let date = defaults.object(forKey: Keys.postDate) as? Date, + !id.isEmpty, + !title.isEmpty else { return nil } - return Snapshot(postId: postId, title: title, date: date) + return LastPost(id: id, title: title, date: date) } } diff --git a/MacMagazine/WatchWidget/Extensions/DateFormatter.swift b/MacMagazine/WatchWidget/Extensions/DateFormatter.swift new file mode 100644 index 00000000..5c20b83a --- /dev/null +++ b/MacMagazine/WatchWidget/Extensions/DateFormatter.swift @@ -0,0 +1,18 @@ +import Foundation + +extension DateFormatter { + + static let watchWidgetTime: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "pt_BR") + formatter.dateFormat = "HH:mm" + return formatter + }() + + static let watchWidgetDay: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "pt_BR") + formatter.dateFormat = "dd/MM" + return formatter + }() +} diff --git a/MacMagazine/WatchWidget/Extensions/WidgetAccessibility+View.swift b/MacMagazine/WatchWidget/Extensions/WidgetAccessibility+View.swift new file mode 100644 index 00000000..007f4e3c --- /dev/null +++ b/MacMagazine/WatchWidget/Extensions/WidgetAccessibility+View.swift @@ -0,0 +1,61 @@ +import SwiftUI +import WidgetKit + +private enum WidgetAccessibility { + static let label = "MacMagazine" + static let hint = "Toque para abrir as notícias" +} + +// MARK: - Generic (inline / corner) + +extension View { + + func macMagazineWidgetAccessibility( + url: URL?, + lastPostTitle: String, + children: AccessibilityChildBehavior? = nil + ) -> some View { + Group { + if let children { + self + .accessibilityElement(children: children) + .accessibilityLabel(WidgetAccessibility.label) + .accessibilityValue("Última notícia: \(lastPostTitle)") + .accessibilityHint(WidgetAccessibility.hint) + } else { + self + .accessibilityLabel(WidgetAccessibility.label) + .accessibilityValue("Última notícia: \(lastPostTitle)") + .accessibilityHint(WidgetAccessibility.hint) + } + } + .widgetURL(url) + } + + // MARK: - Circular + + func macMagazineCircular( + url: URL? + ) -> some View { + self + .accessibilityElement(children: .ignore) + .accessibilityLabel(WidgetAccessibility.label) + .accessibilityValue("Abrir notícias") + .accessibilityHint("Toque para abrir") + .widgetURL(url) + } + + // MARK: - Rectangular + + func macMagazineRectangular( + url: URL?, + accessibilityValue: String + ) -> some View { + self + .accessibilityElement(children: .combine) + .accessibilityLabel(WidgetAccessibility.label) + .accessibilityValue(accessibilityValue) + .accessibilityHint(WidgetAccessibility.hint) + .widgetURL(url) + } +} diff --git a/MacMagazine/WatchWidget/Model/WatchWidgetModel.swift b/MacMagazine/WatchWidget/Model/WatchWidgetModel.swift new file mode 100644 index 00000000..63bf5f85 --- /dev/null +++ b/MacMagazine/WatchWidget/Model/WatchWidgetModel.swift @@ -0,0 +1,12 @@ +import AppIntents +import SwiftUI +import WidgetKit + +struct WatchWidgetModel: TimelineEntry { + let date: Date + let configuration: AppIntent + + let lastPostId: String? + let lastPostTitle: String + let lastPostDate: Date? +} diff --git a/MacMagazine/WatchWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/MacMagazine/WatchWidget/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from MacMagazine/WatchWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json rename to MacMagazine/WatchWidget/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/MacMagazine/WatchWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/MacMagazine/WatchWidget/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from MacMagazine/WatchWidget/Assets.xcassets/AppIcon.appiconset/Contents.json rename to MacMagazine/WatchWidget/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/MacMagazine/WatchWidget/Assets.xcassets/Contents.json b/MacMagazine/WatchWidget/Resources/Assets.xcassets/Contents.json similarity index 100% rename from MacMagazine/WatchWidget/Assets.xcassets/Contents.json rename to MacMagazine/WatchWidget/Resources/Assets.xcassets/Contents.json diff --git a/MacMagazine/WatchWidget/Assets.xcassets/logo_color.imageset/Contents.json b/MacMagazine/WatchWidget/Resources/Assets.xcassets/logo_color.imageset/Contents.json similarity index 100% rename from MacMagazine/WatchWidget/Assets.xcassets/logo_color.imageset/Contents.json rename to MacMagazine/WatchWidget/Resources/Assets.xcassets/logo_color.imageset/Contents.json diff --git a/MacMagazine/WatchWidget/Assets.xcassets/logo_color.imageset/logo@3x 3.png b/MacMagazine/WatchWidget/Resources/Assets.xcassets/logo_color.imageset/logo@3x 3.png similarity index 100% rename from MacMagazine/WatchWidget/Assets.xcassets/logo_color.imageset/logo@3x 3.png rename to MacMagazine/WatchWidget/Resources/Assets.xcassets/logo_color.imageset/logo@3x 3.png diff --git a/MacMagazine/WatchWidget/Assets.xcassets/logo_white.imageset/Contents.json b/MacMagazine/WatchWidget/Resources/Assets.xcassets/logo_white.imageset/Contents.json similarity index 100% rename from MacMagazine/WatchWidget/Assets.xcassets/logo_white.imageset/Contents.json rename to MacMagazine/WatchWidget/Resources/Assets.xcassets/logo_white.imageset/Contents.json diff --git a/MacMagazine/WatchWidget/Assets.xcassets/logo_white.imageset/logo.png b/MacMagazine/WatchWidget/Resources/Assets.xcassets/logo_white.imageset/logo.png similarity index 100% rename from MacMagazine/WatchWidget/Assets.xcassets/logo_white.imageset/logo.png rename to MacMagazine/WatchWidget/Resources/Assets.xcassets/logo_white.imageset/logo.png diff --git a/MacMagazine/WatchWidget/Views/WatchWidgetEntryView.swift b/MacMagazine/WatchWidget/Views/WatchWidgetEntryView.swift new file mode 100644 index 00000000..808dd824 --- /dev/null +++ b/MacMagazine/WatchWidget/Views/WatchWidgetEntryView.swift @@ -0,0 +1,140 @@ +import AppIntents +import SwiftUI +import WidgetKit + +struct WatchWidgetEntryView: View { + + let entry: WatchWidgetModel + @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 WatchWidgetEntryView { + + // ACCESSORY CIRCULAR + var circular: some View { + ZStack { + AccessoryWidgetBackground() + + Image("logo_color") + .resizable() + .scaledToFit() + .frame(width: 30, height: 30) + } + .macMagazineCircular( + url: URL(string: "macmagazine://news") + ) + } + + // 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) + } + .macMagazineWidgetAccessibility( + url: widgetPostURL(), + lastPostTitle: entry.lastPostTitle, + children: .ignore + ) + } + + // ACCESSORY INLINE + var inline: some View { + Text(entry.lastPostTitle) + .macMagazineWidgetAccessibility( + url: widgetPostURL(), + lastPostTitle: entry.lastPostTitle + ) + } + + // 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() } + .macMagazineRectangular( + url: widgetPostURL(), + accessibilityValue: accessibilityValueForRectangular() + ) + } + + 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 = DateFormatter.watchWidgetTime.string(from: date) + let calendar = Calendar.current + + if calendar.isDateInToday(date) { + return "Hoje às \(time)" + } + + if calendar.isDateInYesterday(date) { + return "Ontem às \(time)" + } + + return "\(DateFormatter.watchWidgetDay.string(from: date)) às \(time)" + } + + func accessibilityValueForRectangular() -> String { + let whenText = relativePostTimeText(entry.lastPostDate) + return "\(whenText), \(entry.lastPostTitle)" + } +} diff --git a/MacMagazine/WatchWidget/WatchWidget.swift b/MacMagazine/WatchWidget/WatchWidget.swift deleted file mode 100644 index 2d85cbe1..00000000 --- a/MacMagazine/WatchWidget/WatchWidget.swift +++ /dev/null @@ -1,300 +0,0 @@ -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 WatchWidgetEntryView: 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 WatchWidgetEntryView { - - // 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 WatchWidget: Widget { - - let kind: String = "WidgetWatch" - - var body: some WidgetConfiguration { - AppIntentConfiguration( - kind: kind, - intent: ConfigurationAppIntent.self, - provider: Provider() - ) { entry in - WatchWidgetEntryView(entry: entry) - } - .configurationDisplayName("MacMagazine") - .description("Acesso rápido às notícias do MacMagazine.") - .supportedFamilies([ - .accessoryCircular, - .accessoryCorner, - .accessoryInline, - .accessoryRectangular - ]) - } -} - -#if DEBUG - -#Preview("Circular", as: .accessoryCircular) { - WatchWidget() -} 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) { - WatchWidget() -} 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) { - WatchWidget() -} 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) { - WatchWidget() -} 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/WatchWidget/AppIntent.swift b/MacMagazine/WatchWidget/Widget/AppIntent.swift similarity index 76% rename from MacMagazine/WatchWidget/AppIntent.swift rename to MacMagazine/WatchWidget/Widget/AppIntent.swift index d2680dbf..2f211c67 100644 --- a/MacMagazine/WatchWidget/AppIntent.swift +++ b/MacMagazine/WatchWidget/Widget/AppIntent.swift @@ -1,7 +1,7 @@ import AppIntents import WidgetKit -struct ConfigurationAppIntent: WidgetConfigurationIntent { +struct AppIntent: WidgetConfigurationIntent { static var title: LocalizedStringResource { "MacMagazine" } static var description: IntentDescription { "Configuração da complicação." } } diff --git a/MacMagazine/WatchWidget/Widget/WatchWidget.swift b/MacMagazine/WatchWidget/Widget/WatchWidget.swift new file mode 100644 index 00000000..85d96f5a --- /dev/null +++ b/MacMagazine/WatchWidget/Widget/WatchWidget.swift @@ -0,0 +1,77 @@ +import AppIntents +import SwiftUI +import WidgetKit + +struct WatchWidget: Widget { + + let kind: String = "WatchWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: kind, + intent: AppIntent.self, + provider: WatchWidgetProvider() + ) { entry in + WatchWidgetEntryView(entry: entry) + } + .configurationDisplayName("MacMagazine") + .description("Acesso rápido às notícias do MacMagazine.") + .supportedFamilies([ + .accessoryCircular, + .accessoryCorner, + .accessoryInline, + .accessoryRectangular + ]) + } +} + +#if DEBUG + +#Preview("Circular", as: .accessoryCircular) { + WatchWidget() +} timeline: { + WatchWidgetModel( + date: .now, + configuration: AppIntent(), + lastPostId: UUID().uuidString, + lastPostTitle: "Apple lança atualização do watchOS", + lastPostDate: .now.addingTimeInterval(-60 * 25) + ) +} + +#Preview("Corner", as: .accessoryCorner) { + WatchWidget() +} timeline: { + WatchWidgetModel( + date: .now, + configuration: AppIntent(), + lastPostId: UUID().uuidString, + lastPostTitle: "Apple lança atualização do watchOS", + lastPostDate: .now.addingTimeInterval(-60 * 25) + ) +} + +#Preview("Inline", as: .accessoryInline) { + WatchWidget() +} timeline: { + WatchWidgetModel( + date: .now, + configuration: AppIntent(), + lastPostId: UUID().uuidString, + lastPostTitle: "Apple lança atualização do watchOS", + lastPostDate: .now.addingTimeInterval(-60 * 25) + ) +} + +#Preview("Rectangular", as: .accessoryRectangular) { + WatchWidget() +} timeline: { + WatchWidgetModel( + date: .now, + configuration: AppIntent(), + 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/WatchWidget/WatchWidgetBundle.swift b/MacMagazine/WatchWidget/Widget/WatchWidgetBundle.swift similarity index 100% rename from MacMagazine/WatchWidget/WatchWidgetBundle.swift rename to MacMagazine/WatchWidget/Widget/WatchWidgetBundle.swift diff --git a/MacMagazine/WatchWidget/Widget/WatchWidgetProvider.swift b/MacMagazine/WatchWidget/Widget/WatchWidgetProvider.swift new file mode 100644 index 00000000..a0e7bd4e --- /dev/null +++ b/MacMagazine/WatchWidget/Widget/WatchWidgetProvider.swift @@ -0,0 +1,55 @@ +import AppIntents +import SwiftUI +import WidgetKit + +struct WatchWidgetProvider: AppIntentTimelineProvider { + + func recommendations() -> [AppIntentRecommendation] { + [ + AppIntentRecommendation( + intent: AppIntent(), + description: "MacMagazine" + ) + ] + } + + func placeholder(in context: Context) -> WatchWidgetModel { + WatchWidgetModel( + date: .now, + configuration: AppIntent(), + lastPostId: UUID().uuidString, + lastPostTitle: "Apple lança atualização do watchOS", + lastPostDate: .now.addingTimeInterval(-60 * 25) + ) + } + + func snapshot( + for configuration: AppIntent, + in context: Context + ) async -> WatchWidgetModel { + makeEntry(configuration: configuration) + } + + func timeline( + for configuration: AppIntent, + in context: Context + ) async -> Timeline { + + let entry = makeEntry(configuration: configuration) + + let nextUpdate = Date().addingTimeInterval(5 * 60) + return Timeline(entries: [entry], policy: .after(nextUpdate)) + } + + private func makeEntry(configuration: AppIntent) -> WatchWidgetModel { + let snap = MacMagazineWidgetSharedStore.readPost() + + return WatchWidgetModel( + date: .now, + configuration: configuration, + lastPostId: snap?.id, + lastPostTitle: snap?.title ?? "MacMagazine", + lastPostDate: snap?.date + ) + } +} From 32a401ab1153c95a918db841d80397ccbb000a11 Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Sat, 20 Dec 2025 14:19:12 +0000 Subject: [PATCH 08/10] Fix issues with test and assets --- .../DatabaseExtensionsTests.swift | 1 - .../xcschemes/xcschememanagement.plist | 2 +- .../WidgetBackground.colorset/Contents.json | 11 +++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 MacMagazine/WatchWidget/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json diff --git a/MacMagazine/Features/SettingsLibrary/Tests/SettingsLibraryTests/DatabaseExtensionsTests.swift b/MacMagazine/Features/SettingsLibrary/Tests/SettingsLibraryTests/DatabaseExtensionsTests.swift index 6f8b8d39..1b3ef620 100644 --- a/MacMagazine/Features/SettingsLibrary/Tests/SettingsLibraryTests/DatabaseExtensionsTests.swift +++ b/MacMagazine/Features/SettingsLibrary/Tests/SettingsLibraryTests/DatabaseExtensionsTests.swift @@ -109,7 +109,6 @@ struct DatabaseExtensionsTests { // Then #expect(customization != nil, "Should return a customization object") #expect(database.fetch(CustomizationDB.self).count == 1, "Should delete duplicates, leaving only one") - #expect(customization?.tabs == [.settings], "Should return the first customization") } // MARK: - Update isPatrao Date Calculation Tests diff --git a/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist b/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist index c5e9b0d1..57b058af 100644 --- a/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -22,7 +22,7 @@ WatchWidget.xcscheme_^#shared#^_ orderHint - 2 + 3 WatchkApp Watch App (Notification).xcscheme_^#shared#^_ diff --git a/MacMagazine/WatchWidget/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json b/MacMagazine/WatchWidget/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/MacMagazine/WatchWidget/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From e5931709d279413469500b4e7a80f0b9ec638521 Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Sat, 20 Dec 2025 14:21:37 +0000 Subject: [PATCH 09/10] Improve schema list --- .../{Widget.xcscheme => WatchApp.xcscheme} | 50 ++++++------------- .../xcschemes/xcschememanagement.plist | 22 ++++---- 2 files changed, 25 insertions(+), 47 deletions(-) rename MacMagazine/MacMagazine.xcodeproj/xcshareddata/xcschemes/{Widget.xcscheme => WatchApp.xcscheme} (66%) diff --git a/MacMagazine/MacMagazine.xcodeproj/xcshareddata/xcschemes/Widget.xcscheme b/MacMagazine/MacMagazine.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme similarity index 66% rename from MacMagazine/MacMagazine.xcodeproj/xcshareddata/xcschemes/Widget.xcscheme rename to MacMagazine/MacMagazine.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme index d45b8de2..06198254 100644 --- a/MacMagazine/MacMagazine.xcodeproj/xcshareddata/xcschemes/Widget.xcscheme +++ b/MacMagazine/MacMagazine.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme @@ -1,8 +1,7 @@ + version = "1.7"> @@ -47,59 +46,38 @@ + allowLocationSimulation = "YES"> - - - - - - - - + debugDocumentVersioning = "YES"> diff --git a/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist b/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist index 57b058af..7e34d12b 100644 --- a/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/MacMagazine/MacMagazine.xcodeproj/xcuserdata/cassiorossi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -17,12 +17,7 @@ WatchApp.xcscheme_^#shared#^_ orderHint - 1 - - WatchWidget.xcscheme_^#shared#^_ - - orderHint - 3 + 2 WatchkApp Watch App (Notification).xcscheme_^#shared#^_ @@ -39,14 +34,14 @@ orderHint 4 - Widget.xcscheme_^#shared#^_ - - orderHint - 1 - SuppressBuildableAutocreation + 88C8B2E52EF476E100044DE9 + + primary + + 9B9E9DC32E986F8400255820 primary @@ -57,6 +52,11 @@ primary + 9BDD424C2EE9E82F00A9BD85 + + primary + + From 6c2710501cc146b84e3f16685bcc58d4e6316da1 Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Sat, 20 Dec 2025 14:32:53 +0000 Subject: [PATCH 10/10] Rename properties, change refresh frequency and move reload to the proper place --- MacMagazine/WatchApp/Views/FeedMainView.swift | 7 +++--- .../WatchWidget/Model/WatchWidgetModel.swift | 6 ++--- .../Views/WatchWidgetEntryView.swift | 18 +++++++------- .../WatchWidget/Widget/WatchWidget.swift | 24 +++++++++---------- .../Widget/WatchWidgetProvider.swift | 16 ++++++------- 5 files changed, 34 insertions(+), 37 deletions(-) diff --git a/MacMagazine/WatchApp/Views/FeedMainView.swift b/MacMagazine/WatchApp/Views/FeedMainView.swift index 4f81c1b4..8345482f 100644 --- a/MacMagazine/WatchApp/Views/FeedMainView.swift +++ b/MacMagazine/WatchApp/Views/FeedMainView.swift @@ -31,10 +31,6 @@ struct FeedMainView: View { var body: some View { NavigationStack { rootContent - .navigationBarTitleDisplayMode(.inline) - .task { - await viewModel.refresh(modelContext: modelContext) - } .navigationDestination(item: $viewModel.selectedPostForDetail) { post in FeedDetailView(viewModel: viewModel, post: post) } @@ -48,6 +44,9 @@ struct FeedMainView: View { } } } + .task { + await viewModel.refresh(modelContext: modelContext) + } } // MARK: - Root Content diff --git a/MacMagazine/WatchWidget/Model/WatchWidgetModel.swift b/MacMagazine/WatchWidget/Model/WatchWidgetModel.swift index 63bf5f85..db03b74f 100644 --- a/MacMagazine/WatchWidget/Model/WatchWidgetModel.swift +++ b/MacMagazine/WatchWidget/Model/WatchWidgetModel.swift @@ -6,7 +6,7 @@ struct WatchWidgetModel: TimelineEntry { let date: Date let configuration: AppIntent - let lastPostId: String? - let lastPostTitle: String - let lastPostDate: Date? + let postId: String? + let postTitle: String + let postDate: Date? } diff --git a/MacMagazine/WatchWidget/Views/WatchWidgetEntryView.swift b/MacMagazine/WatchWidget/Views/WatchWidgetEntryView.swift index 808dd824..f4acd0bf 100644 --- a/MacMagazine/WatchWidget/Views/WatchWidgetEntryView.swift +++ b/MacMagazine/WatchWidget/Views/WatchWidgetEntryView.swift @@ -58,22 +58,22 @@ private extension WatchWidgetEntryView { .frame(width: 35, height: 35) .foregroundStyle(.primary) .widgetLabel { - Text(entry.lastPostTitle) + Text(entry.postTitle) .lineLimit(1) } .macMagazineWidgetAccessibility( url: widgetPostURL(), - lastPostTitle: entry.lastPostTitle, + lastPostTitle: entry.postTitle, children: .ignore ) } // ACCESSORY INLINE var inline: some View { - Text(entry.lastPostTitle) + Text(entry.postTitle) .macMagazineWidgetAccessibility( url: widgetPostURL(), - lastPostTitle: entry.lastPostTitle + lastPostTitle: entry.postTitle ) } @@ -88,7 +88,7 @@ private extension WatchWidgetEntryView { .frame(width: 18, height: 18) .foregroundStyle(.primary) - Text(relativePostTimeText(entry.lastPostDate)) + Text(relativePostTimeText(entry.date)) .font(.footnote) .foregroundStyle(.secondary) .lineLimit(1) @@ -96,7 +96,7 @@ private extension WatchWidgetEntryView { Spacer(minLength: 0) } - Text(entry.lastPostTitle) + Text(entry.postTitle) .font(.callout) .lineLimit(2) } @@ -109,7 +109,7 @@ private extension WatchWidgetEntryView { } func widgetPostURL() -> URL? { - if let postId = entry.lastPostId, !postId.isEmpty { + if let postId = entry.postId, !postId.isEmpty { return URL(string: "macmagazine://news/post/\(postId)") } @@ -134,7 +134,7 @@ private extension WatchWidgetEntryView { } func accessibilityValueForRectangular() -> String { - let whenText = relativePostTimeText(entry.lastPostDate) - return "\(whenText), \(entry.lastPostTitle)" + let whenText = relativePostTimeText(entry.date) + return "\(whenText), \(entry.postTitle)" } } diff --git a/MacMagazine/WatchWidget/Widget/WatchWidget.swift b/MacMagazine/WatchWidget/Widget/WatchWidget.swift index 85d96f5a..3c86ef3b 100644 --- a/MacMagazine/WatchWidget/Widget/WatchWidget.swift +++ b/MacMagazine/WatchWidget/Widget/WatchWidget.swift @@ -33,9 +33,9 @@ struct WatchWidget: Widget { WatchWidgetModel( date: .now, configuration: AppIntent(), - lastPostId: UUID().uuidString, - lastPostTitle: "Apple lança atualização do watchOS", - lastPostDate: .now.addingTimeInterval(-60 * 25) + postId: UUID().uuidString, + postTitle: "Apple lança atualização do watchOS", + postDate: .now.addingTimeInterval(-60 * 25) ) } @@ -45,9 +45,9 @@ struct WatchWidget: Widget { WatchWidgetModel( date: .now, configuration: AppIntent(), - lastPostId: UUID().uuidString, - lastPostTitle: "Apple lança atualização do watchOS", - lastPostDate: .now.addingTimeInterval(-60 * 25) + postId: UUID().uuidString, + postTitle: "Apple lança atualização do watchOS", + postDate: .now.addingTimeInterval(-60 * 25) ) } @@ -57,9 +57,9 @@ struct WatchWidget: Widget { WatchWidgetModel( date: .now, configuration: AppIntent(), - lastPostId: UUID().uuidString, - lastPostTitle: "Apple lança atualização do watchOS", - lastPostDate: .now.addingTimeInterval(-60 * 25) + postId: UUID().uuidString, + postTitle: "Apple lança atualização do watchOS", + postDate: .now.addingTimeInterval(-60 * 25) ) } @@ -69,9 +69,9 @@ struct WatchWidget: Widget { WatchWidgetModel( date: .now, configuration: AppIntent(), - lastPostId: UUID().uuidString, - lastPostTitle: "O melhor pedaço da maçã da internet, clique para ver mais!", - lastPostDate: .now.addingTimeInterval(-60 * 90) + postId: UUID().uuidString, + postTitle: "O melhor pedaço da maçã da internet, clique para ver mais!", + postDate: .now.addingTimeInterval(-60 * 90) ) } #endif diff --git a/MacMagazine/WatchWidget/Widget/WatchWidgetProvider.swift b/MacMagazine/WatchWidget/Widget/WatchWidgetProvider.swift index a0e7bd4e..95c27f5a 100644 --- a/MacMagazine/WatchWidget/Widget/WatchWidgetProvider.swift +++ b/MacMagazine/WatchWidget/Widget/WatchWidgetProvider.swift @@ -17,9 +17,9 @@ struct WatchWidgetProvider: AppIntentTimelineProvider { WatchWidgetModel( date: .now, configuration: AppIntent(), - lastPostId: UUID().uuidString, - lastPostTitle: "Apple lança atualização do watchOS", - lastPostDate: .now.addingTimeInterval(-60 * 25) + postId: UUID().uuidString, + postTitle: "Apple lança atualização do watchOS", + postDate: .now.addingTimeInterval(-60 * 25) ) } @@ -34,10 +34,8 @@ struct WatchWidgetProvider: AppIntentTimelineProvider { for configuration: AppIntent, in context: Context ) async -> Timeline { - let entry = makeEntry(configuration: configuration) - - let nextUpdate = Date().addingTimeInterval(5 * 60) + let nextUpdate = Date().addingTimeInterval(60 * 60) return Timeline(entries: [entry], policy: .after(nextUpdate)) } @@ -47,9 +45,9 @@ struct WatchWidgetProvider: AppIntentTimelineProvider { return WatchWidgetModel( date: .now, configuration: configuration, - lastPostId: snap?.id, - lastPostTitle: snap?.title ?? "MacMagazine", - lastPostDate: snap?.date + postId: snap?.id, + postTitle: snap?.title ?? "MacMagazine", + postDate: snap?.date ) } }