diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/Contents.json b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/alternativa_sem_fundo.imageset/Contents.json b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/alternativa_without_background.imageset/Contents.json similarity index 100% rename from MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/alternativa_sem_fundo.imageset/Contents.json rename to MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/alternativa_without_background.imageset/Contents.json diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/alternativa_sem_fundo.imageset/alternativa_sem_fundo.png b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/alternativa_without_background.imageset/alternativa_sem_fundo.png similarity index 100% rename from MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/alternativa_sem_fundo.imageset/alternativa_sem_fundo.png rename to MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/alternativa_without_background.imageset/alternativa_sem_fundo.png diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/normal_sem_fundo.imageset/Contents.json b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/normal_without_background.imageset/Contents.json similarity index 100% rename from MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/normal_sem_fundo.imageset/Contents.json rename to MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/normal_without_background.imageset/Contents.json diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/normal_sem_fundo.imageset/normal_sem_fundo.png b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/normal_without_background.imageset/normal_sem_fundo.png similarity index 100% rename from MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/normal_sem_fundo.imageset/normal_sem_fundo.png rename to MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Resources/Assets.xcassets/icons without background/normal_without_background.imageset/normal_sem_fundo.png diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/ViewModels/OnboardingCoordinator.swift b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/ViewModels/OnboardingCoordinator.swift index 3a927ce4..7c4291c9 100644 --- a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/ViewModels/OnboardingCoordinator.swift +++ b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/ViewModels/OnboardingCoordinator.swift @@ -6,7 +6,9 @@ import SwiftUI @MainActor @Observable -public final class OnboardingCoordinator { +public final class OnboardingCoordinator: @MainActor Identifiable { + public var id: String { "onboarding" } + // Current screen state public var currentScreen: OnboardingScreen = .welcome @@ -17,8 +19,9 @@ public final class OnboardingCoordinator { public let permissionManager: PermissionManager public let analytics: AnalyticsManager - // UserDefaults key + // UserDefaults keys private static let hasCompletedOnboardingKey = "hasCompletedOnboarding" + private static let hasSeenFeaturesKey = "hasSeenOnboardingFeatures" public init( permissionManager: PermissionManager, @@ -34,6 +37,10 @@ public final class OnboardingCoordinator { withAnimation(.easeInOut(duration: 0.3)) { currentScreen = screen } + + if screen == .permissions { + markFeaturesAsSeen() + } } func skipToPermissions() { @@ -42,6 +49,8 @@ public final class OnboardingCoordinator { screen: currentScreen.analyticsName )) + markFeaturesAsSeen() + withAnimation(.easeInOut(duration: 0.3)) { currentScreen = .permissions } @@ -61,6 +70,16 @@ public final class OnboardingCoordinator { onComplete?() } + // MARK: - Features Seen State + + private func markFeaturesAsSeen() { + UserDefaults.standard.set(true, forKey: Self.hasSeenFeaturesKey) + } + + private static var hasSeenFeatures: Bool { + UserDefaults.standard.bool(forKey: hasSeenFeaturesKey) + } + // MARK: - Onboarding State Management /// Create coordinator if onboarding is needed, returns nil if not needed @@ -71,12 +90,19 @@ public final class OnboardingCoordinator { // Check if onboarding was completed let hasCompleted = UserDefaults.standard.bool(forKey: hasCompletedOnboardingKey) - // If never completed, create coordinator starting at welcome + // If never completed, create coordinator if !hasCompleted { let coordinator = OnboardingCoordinator( permissionManager: permissionManager, analytics: analytics ) + + // If user already saw features, go directly to permissions + if hasSeenFeatures { + coordinator.currentScreen = .permissions + } + // Otherwise, start from welcome + return coordinator } @@ -86,7 +112,7 @@ public final class OnboardingCoordinator { permissionManager: permissionManager, analytics: analytics ) - // Start at permissions screen + // Start at permissions screen (already completed before, no need to see features again) coordinator.currentScreen = .permissions return coordinator } @@ -98,6 +124,7 @@ public final class OnboardingCoordinator { /// Reset onboarding state (for debug/testing) public static func resetOnboarding() { UserDefaults.standard.removeObject(forKey: hasCompletedOnboardingKey) + UserDefaults.standard.removeObject(forKey: hasSeenFeaturesKey) } } diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/OnboardingBackground.swift b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/OnboardingBackground.swift index 9cdc93f2..e293c6bf 100644 --- a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/OnboardingBackground.swift +++ b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/OnboardingBackground.swift @@ -10,7 +10,7 @@ public struct OnboardingBackground: View { (theme.main.background.color ?? Color.secondary) .ignoresSafeArea() - Image("normal_sem_fundo", bundle: .module) + Image("normal_without_background", bundle: .module) .resizable() .scaledToFit() .frame(width: 520) @@ -20,13 +20,13 @@ public struct OnboardingBackground: View { .blur(radius: 5) .accessibilityHidden(true) - Image("alternativa_sem_fundo", bundle: .module) + Image("alternativa_without_background", bundle: .module) .resizable() .scaledToFit() .frame(width: 360) .rotationEffect(.degrees(18)) .offset(x: -170, y: 220) - .opacity(colorScheme == .dark ? 0.4 : 0.9) + .opacity(colorScheme == .dark ? 0.14 : 0.14) .blur(radius: 5) .accessibilityHidden(true) diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/OnboardingLogoView.swift b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/OnboardingLogoView.swift index aa22e591..c7d621a1 100644 --- a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/OnboardingLogoView.swift +++ b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/OnboardingLogoView.swift @@ -5,8 +5,14 @@ public struct OnboardingLogoView: View { let width: CGFloat let height: CGFloat - public init(width: CGFloat = 152, - height: CGFloat = 152) { + private var cornerRadius: CGFloat { + min(width, height) * 0.184 + } + + public init( + width: CGFloat = 152, + height: CGFloat = 152 + ) { self.width = width self.height = height } @@ -15,17 +21,31 @@ public struct OnboardingLogoView: View { Image("normal", bundle: .module) .resizable() .scaledToFit() - .frame(width: self.width, height: self.height) - .clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous)) + .frame(width: width, height: height) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) .overlay { - RoundedRectangle(cornerRadius: 28, style: .continuous) + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .stroke(.white.opacity(0.15), lineWidth: 1) } .background { - RoundedRectangle(cornerRadius: 28, style: .continuous) + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .fill(.regularMaterial) .shadow(radius: 18, y: 10) } .accessibilityHidden(true) } } + +// MARK: - Preview + +#if DEBUG +#Preview("Logo Sizes") { + VStack(spacing: 40) { + OnboardingLogoView(width: 152, height: 152) + OnboardingLogoView(width: 80, height: 80) + OnboardingLogoView(width: 60, height: 60) + } + .padding() + .background(Color.gray.opacity(0.3)) +} +#endif diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/PermissionView.swift b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/PermissionCard.swift similarity index 81% rename from MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/PermissionView.swift rename to MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/PermissionCard.swift index 92c85585..e3798a1e 100644 --- a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/PermissionView.swift +++ b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Components/PermissionCard.swift @@ -25,6 +25,7 @@ public struct PermissionCard: View { public var body: some View { VStack(alignment: .leading, spacing: 12) { + // Header com ícone e título HStack(spacing: 12) { Image(systemName: icon) .font(.title2) @@ -37,15 +38,17 @@ public struct PermissionCard: View { .foregroundStyle(.primary) Spacer() - - statusView } - .accessibilityElement(children: .combine) + // Descrição Text(description) .font(.subheadline) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) + + // Botão de status + statusView + .frame(maxWidth: .infinity, alignment: .trailing) } .padding(16) .frame(maxWidth: .infinity, alignment: .leading) @@ -53,6 +56,7 @@ public struct PermissionCard: View { RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(.ultraThinMaterial) ) + .accessibilityElement(children: .combine) } @ViewBuilder @@ -84,32 +88,37 @@ public struct PermissionCard: View { } .buttonStyle(.plain) .disabled(isProcessing) + .accessibilityLabel("Permitir \(title)") + .accessibilityHint("Toque para autorizar esta permissão") case .granted: HStack(spacing: 6) { Image(systemName: "checkmark.circle.fill") - .font(.caption) + .font(.subheadline) Text("Autorizado") - .font(.caption.weight(.medium)) + .font(.subheadline.weight(.semibold)) } .foregroundStyle(.white) - .padding(.horizontal, 12) - .padding(.vertical, 6) + .padding(.horizontal, 16) + .padding(.vertical, 8) .background(Color.green) .clipShape(Capsule()) + .accessibilityLabel("\(title) autorizado") case .denied: HStack(spacing: 6) { Image(systemName: "xmark.circle.fill") - .font(.caption) + .font(.subheadline) Text("Negado") - .font(.caption.weight(.medium)) + .font(.subheadline.weight(.semibold)) } .foregroundStyle(.white) - .padding(.horizontal, 12) - .padding(.vertical, 6) + .padding(.horizontal, 16) + .padding(.vertical, 8) .background(Color.gray) .clipShape(Capsule()) + .accessibilityLabel("\(title) negado") + .accessibilityHint("Você pode alterar nas Configurações do dispositivo") } } } @@ -124,7 +133,7 @@ public enum PermissionCardStatus { // MARK: - Preview -#Preview("Permission Card - Not Determined") { +#Preview("Permission Card - All States") { VStack(spacing: 16) { PermissionCard( title: "Notificações", diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/OnboardingContainerView.swift b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/OnboardingContainerView.swift index 9c18b099..5adf44a8 100644 --- a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/OnboardingContainerView.swift +++ b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/OnboardingContainerView.swift @@ -19,7 +19,7 @@ public struct OnboardingContainerView: View { public var body: some View { NavigationStack { ZStack { - OnboardingBackground() +// OnboardingBackground() Group { switch coordinator.currentScreen { @@ -48,7 +48,7 @@ public struct OnboardingContainerView: View { OnboardingSheetPreviewHost() } -#Preview("Sheet - iPad", traits: .landscapeLeft) { +#Preview("Sheet - Landscape", traits: .landscapeLeft) { OnboardingSheetPreviewHost() } diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen1_WelcomeView.swift b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen1_WelcomeView.swift index 575f418a..4b2c1a88 100644 --- a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen1_WelcomeView.swift +++ b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen1_WelcomeView.swift @@ -36,7 +36,7 @@ struct WelcomeView: View { .padding(.top, 16) .padding(.trailing, 20) } - .background(OnboardingBackground()) +// .background(OnboardingBackground()) .onAppear { animateIn = true } .onDisappear { animateIn = false } .trackScreen( diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen2_FeaturesView.swift b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen2_FeaturesView.swift index f43c9e82..602f2190 100644 --- a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen2_FeaturesView.swift +++ b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen2_FeaturesView.swift @@ -46,10 +46,6 @@ struct FeaturesView: View { currentPage >= max(0, pages.count - 1) } - private var isFirstPage: Bool { - currentPage == 0 - } - // MARK: - Body var body: some View { @@ -60,10 +56,7 @@ struct FeaturesView: View { portraitLayout } } - .padding(.horizontal, 20) .containerRelativeFrame([.horizontal, .vertical]) - .contentShape(Rectangle()) - .gesture(pageSwipeGesture) .overlay(alignment: .topTrailing) { if !isLastPage { OnboardingSkipButton( @@ -85,59 +78,49 @@ struct FeaturesView: View { .trackScreen(AnalyticsConstants.Screen.onboardingFeatures.name, analytics: coordinator.analytics) } - // MARK: - Gestures - - private var pageSwipeGesture: some Gesture { - DragGesture(minimumDistance: 50) - .onEnded { value in - let horizontal = value.translation.width - - if horizontal > 0 && !isFirstPage { - withAnimation(.smooth) { - currentPage -= 1 - } - } else if horizontal < 0 && !isLastPage { - withAnimation(.smooth) { - currentPage += 1 - } - } - } - } - // MARK: - Portrait Layout private var portraitLayout: some View { VStack(spacing: 16) { OnboardingLogoView(width: 80, height: 80) + .matchedGeometryEffect(id: "onboarding_logo", in: logoNamespace) .padding(.top, 80) + .accessibilityHidden(true) OnboardingTitleView("Novidades no App MacMagazine", animateIn: animateIn) - Spacer() + Spacer(minLength: 16) + + paginatedCardsView - cardsGrid + pageIndicator + .padding(.top, 8) - Spacer() + Spacer(minLength: 16) footerSection + .padding(.bottom, 20) } + .padding(.horizontal, 20) } // MARK: - Landscape Layout private var landscapeLayout: some View { HStack(spacing: 24) { - // Lado esquerdo: Logo e título (centralizado verticalmente) VStack(spacing: 12) { Spacer() OnboardingLogoView(width: 80, height: 80) + .matchedGeometryEffect(id: "onboarding_logo", in: logoNamespace) + .accessibilityHidden(true) Text("Novidades no App") .font(.headline) .fontWeight(.bold) .multilineTextAlignment(.center) .opacity(animateIn ? 1 : 0) + .accessibilityAddTraits(.isHeader) Spacer() @@ -146,43 +129,91 @@ struct FeaturesView: View { .frame(maxWidth: 180) .padding(.leading, -40) - // Lado direito: Cards e navegação VStack(spacing: 12) { Spacer() - cardsGrid + paginatedCardsView + + pageIndicator Spacer() HStack { - if pages.count > 1 { - pageIndicator - } - Spacer() - continueButton + .opacity(isLastPage ? 1 : 0) + .offset(y: isLastPage ? 0 : 20) + .animation(.easeInOut(duration: 0.4), value: isLastPage) } } .padding(.top, 40) } + .padding(.horizontal, 20) .padding(.vertical, 16) } - // MARK: - Componentes Compartilhados - - private var cardsGrid: some View { - LazyVGrid(columns: gridColumns, spacing: 16) { - let pageCards = pages.count > 1 ? pages[currentPage] : features + // MARK: - Paginated Cards with Native TabView - ForEach(Array(pageCards.enumerated()), id: \.offset) { index, card in - cardView(card: card, index: index) + private var paginatedCardsView: some View { + TabView(selection: $currentPage) { + ForEach(Array(pages.enumerated()), id: \.offset) { pageIndex, pageCards in + pageView(cards: pageCards) + .tag(pageIndex) } } + .tabViewStyle(.page(indexDisplayMode: .never)) + .frame(height: tabViewHeight) + .onboardingAnimateIn(animateIn, delay: 0.2, reduceMotion: reduceMotion) .accessibilityLabel("Lista de novidades, página \(currentPage + 1) de \(pages.count)") } - private func cardView(card: OnBoardingFeature, index: Int) -> some View { + private var tabViewHeight: CGFloat { + if isLandscape { + return 200 + } + if horizontalSizeClass == .regular { + return 280 + } + return 320 + } + + // MARK: - Custom Page Indicator + + private var pageIndicator: some View { + HStack(spacing: 8) { + ForEach(0.. some View { + LazyVGrid(columns: gridColumns, spacing: 16) { + ForEach(cards) { card in + cardView(card: card) + } + } + .padding(.horizontal, 8) + } + + private func cardView(card: OnBoardingFeature) -> some View { HStack(alignment: .top, spacing: 12) { Image(systemName: card.symbol) .font(.title2) @@ -206,31 +237,13 @@ struct FeaturesView: View { Spacer(minLength: 0) } .frame(maxHeight: .infinity, alignment: .top) - .onboardingAnimateIn(animateIn, delay: 0.2 + Double(index) * 0.1, reduceMotion: reduceMotion) .accessibilityElement(children: .combine) } - private var pageIndicator: some View { - HStack(spacing: 6) { - ForEach(0.. 1 { - pageIndicator - } - VStack(alignment: .leading, spacing: 6) { Image(systemName: "person.3.fill") .foregroundStyle(.primary) @@ -243,11 +256,13 @@ struct FeaturesView: View { .fixedSize(horizontal: false, vertical: true) } .frame(maxWidth: .infinity, alignment: .leading) - .onboardingAnimateIn(animateIn, delay: 0.7, reduceMotion: reduceMotion) continueButton - .padding(.bottom, 16) } + .frame(minHeight: 100) + .opacity(isLastPage ? 1 : 0) + .offset(y: isLastPage ? 0 : 30) + .animation(.easeInOut(duration: 0.4), value: isLastPage) } private var compactFooterSection: some View { @@ -263,31 +278,24 @@ struct FeaturesView: View { .lineLimit(2) } .frame(maxWidth: .infinity, alignment: .leading) - .opacity(animateIn ? 1 : 0) + .opacity(isLastPage ? 1 : 0) + .offset(y: isLastPage ? 0 : 20) + .animation(.easeInOut(duration: 0.4), value: isLastPage) } private var continueButton: some View { - OnboardingCTAButton( - isLastPage ? "Continuar" : "Avançar", - showChevron: !isLastPage - ) { + OnboardingCTAButton("Continuar") { handleContinue() } .onboardingAnimateIn(animateIn, delay: 0.8, reduceMotion: reduceMotion) - .accessibilityLabel(isLastPage ? "Continuar para permissões" : "Avançar para próxima página") - .accessibilityHint(isLastPage ? "Vai para a tela de permissões" : "Mostra mais novidades") + .accessibilityLabel("Continuar para permissões") + .accessibilityHint("Vai para a tela de permissões") } // MARK: - Actions private func handleContinue() { - if currentPage < pages.count - 1 { - withAnimation(reduceMotion ? nil : .smooth) { - currentPage += 1 - } - } else { - coordinator.navigate(to: .permissions) - } + coordinator.navigate(to: .permissions) } } diff --git a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen3_PermissionsView.swift b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen3_PermissionsView.swift index 395efebc..78f8b4f5 100644 --- a/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen3_PermissionsView.swift +++ b/MacMagazine/Features/OnboardingLibrary/Sources/OnboardingLibrary/Views/Screen3_PermissionsView.swift @@ -69,7 +69,7 @@ struct PermissionsView: View { portraitLayout } } - .background(OnboardingBackground()) +// .background(OnboardingBackground()) .onAppear { withAnimation { animateIn = true diff --git a/MacMagazine/MacMagazine/MainApp/MainViewModel.swift b/MacMagazine/MacMagazine/MainApp/MainViewModel.swift index 2bb61650..de5522cc 100644 --- a/MacMagazine/MacMagazine/MainApp/MainViewModel.swift +++ b/MacMagazine/MacMagazine/MainApp/MainViewModel.swift @@ -81,7 +81,3 @@ class MainViewModel { onboardingCoordinator != nil } } - -extension OnboardingCoordinator: @MainActor @retroactive Identifiable { - public var id: String { "onboarding" } -}