From 87a0806eb4ecfd49bd0015bd26457a1708bda9c9 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Wed, 17 Dec 2025 18:08:21 -0300 Subject: [PATCH 1/4] Adds Apple Watch app with basic feed Adds the initial structure for the Apple Watch app, including: - Data models and extensions for handling feed items. - A scrollable feed display with navigation and detail views. - UI components like a dots indicator and flow tags. - View models for managing the feed data and user interactions. --- .../ViewModel/MMLiveViewModel.swift | 2 +- MacMagazine/WatchApp/Extension/FeedDB.swift | 66 ++++++ .../Helper/FeedDotsIndicatorView.swift | 65 ++++++ .../WatchApp/Helper/FlowTagsView.swift | 106 ++++++++++ MacMagazine/WatchApp/Model/SelectedPost.swift | 14 ++ MacMagazine/WatchApp/Resources/WatchApp.swift | 22 ++ .../ViewModel/FeedRootViewModel.swift | 151 ++++++++++++++ .../ViewModel/FeedScrollPositionKey.swift | 36 ++++ .../WatchApp/Views/FeedDetailView.swift | 109 ++++++++++ MacMagazine/WatchApp/Views/FeedRootView.swift | 197 ++++++++++++++++++ .../Views/Row/FeedFullScreenRowView.swift | 84 ++++++++ .../WatchApp/Views/Row/FeedRowView.swift | 113 ++++++++++ MacMagazine/WatchApp/WatchApp.swift | 10 - 13 files changed, 964 insertions(+), 11 deletions(-) create mode 100644 MacMagazine/WatchApp/Extension/FeedDB.swift create mode 100644 MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift create mode 100644 MacMagazine/WatchApp/Helper/FlowTagsView.swift create mode 100644 MacMagazine/WatchApp/Model/SelectedPost.swift create mode 100644 MacMagazine/WatchApp/Resources/WatchApp.swift create mode 100644 MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift create mode 100644 MacMagazine/WatchApp/ViewModel/FeedScrollPositionKey.swift create mode 100644 MacMagazine/WatchApp/Views/FeedDetailView.swift create mode 100644 MacMagazine/WatchApp/Views/FeedRootView.swift create mode 100644 MacMagazine/WatchApp/Views/Row/FeedFullScreenRowView.swift create mode 100644 MacMagazine/WatchApp/Views/Row/FeedRowView.swift delete mode 100644 MacMagazine/WatchApp/WatchApp.swift diff --git a/MacMagazine/Features/MMLiveLibrary/Sources/MMLiveLibrary/ViewModel/MMLiveViewModel.swift b/MacMagazine/Features/MMLiveLibrary/Sources/MMLiveLibrary/ViewModel/MMLiveViewModel.swift index 9d5639bf..1e2ad451 100644 --- a/MacMagazine/Features/MMLiveLibrary/Sources/MMLiveLibrary/ViewModel/MMLiveViewModel.swift +++ b/MacMagazine/Features/MMLiveLibrary/Sources/MMLiveLibrary/ViewModel/MMLiveViewModel.swift @@ -41,7 +41,7 @@ private extension MMLiveViewModel { var event = event event.lastChecked = Date() storage.save(event: event) - return true // Date() > event.inicio && Date() < event.fim + return Date() > event.inicio && Date() < event.fim } } diff --git a/MacMagazine/WatchApp/Extension/FeedDB.swift b/MacMagazine/WatchApp/Extension/FeedDB.swift new file mode 100644 index 00000000..c0229389 --- /dev/null +++ b/MacMagazine/WatchApp/Extension/FeedDB.swift @@ -0,0 +1,66 @@ +import FeedLibrary +import Foundation + +extension FeedDB { + var linkURL: URL? { + guard !link.isEmpty else { return nil } + return URL(string: link) + } + + var dateText: String { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "pt_BR") + formatter.dateFormat = "dd/MM/yyyy 'às' HH:mm" + return formatter.string(from: pubDate) + } + + var displaySubtitle: String? { + let subtitle = subtitle.trimmingCharacters(in: .whitespacesAndNewlines) + return subtitle.isEmpty ? nil : subtitle + } + + var displayBody: String? { + let full = fullContent.trimmingCharacters(in: .whitespacesAndNewlines) + if !full.isEmpty { return full } + + let excerpt = excerpt.trimmingCharacters(in: .whitespacesAndNewlines) + return excerpt.isEmpty ? nil : excerpt + } + + var artworkRemoteURL: URL? { + guard !artworkURL.isEmpty else { return nil } + return URL(string: artworkURL) + } + + static var previewItem: FeedDB { + FeedDB( + postId: UUID().uuidString, + title: "Apple lança atualização do watchOS", + subtitle: "Mudanças importantes para o Apple Watch", + pubDate: Date().addingTimeInterval(-3600), + artworkURL: "https://picsum.photos/400/400", + link: "https://macmagazine.com.br", + categories: ["watchos", "news", "teste1", "teste2", "teste 3"], + excerpt: "Resumo curto para teste no relógio…", + fullContent: "", + favorite: false + ) + } + + static var previewItems: [FeedDB] { + (1...10).map { index in + FeedDB( + postId: UUID().uuidString, + title: "Notícia \(index): título de teste para o Watch", + subtitle: "Subtítulo \(index)", + pubDate: Date().addingTimeInterval(TimeInterval(-index * 900)), + artworkURL: "https://picsum.photos/seed/\(index)/600/600", + link: "https://macmagazine.com.br", + categories: ["news", "teste1", "teste2", "teste 3"], + excerpt: "Excerpt \(index) – texto curto para validar layout.", + fullContent: "", + favorite: index % 3 == 0 + ) + } + } +} diff --git a/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift b/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift new file mode 100644 index 00000000..0eae6206 --- /dev/null +++ b/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift @@ -0,0 +1,65 @@ +import SwiftUI + +enum IndicatorAxis { + case horizontal + case vertical +} + +// MARK: - Dots Indicator + +public struct FeedDotsIndicatorView: View { + let axis: IndicatorAxis + let count: Int + let selectedIndex: Int + + public var body: some View { + Group { + switch axis { + case .vertical: + VStack(spacing: 5) { + dots + } + + case .horizontal: + HStack(spacing: 6) { + dots + } + } + } + .padding(6) + .glassEffect(.clear) + .allowsHitTesting(false) + } + + private var dots: some View { + ForEach(0.. Color { + index == selectedIndex ? .white : .white.opacity(0.4) + } + + private func dotSize(for index: Int) -> CGFloat { + index == selectedIndex ? 8 : 5 + } +} + +#Preview("Vertical") { + ZStack { + Color.gray + FeedDotsIndicatorView(axis: .vertical, count: 10, selectedIndex: 3) + } +} + +#Preview("Horizontal") { + ZStack { + Color.gray + FeedDotsIndicatorView(axis: .horizontal, count: 5, selectedIndex: 2) + } +} diff --git a/MacMagazine/WatchApp/Helper/FlowTagsView.swift b/MacMagazine/WatchApp/Helper/FlowTagsView.swift new file mode 100644 index 00000000..328d5a51 --- /dev/null +++ b/MacMagazine/WatchApp/Helper/FlowTagsView.swift @@ -0,0 +1,106 @@ +import SwiftUI + +struct FlowTagsView: View { + + // MARK: - Properties + + let tags: [Tag] + let horizontalSpacing: CGFloat + let verticalSpacing: CGFloat + let content: (Tag) -> Content + + @State private var measuredHeight: CGFloat = 0 + + // MARK: - Init + + init( + tags: [Tag], + horizontalSpacing: CGFloat, + verticalSpacing: CGFloat, + @ViewBuilder content: @escaping (Tag) -> Content + ) { + self.tags = tags + self.horizontalSpacing = horizontalSpacing + self.verticalSpacing = verticalSpacing + self.content = content + } + + // MARK: - Body + + var body: some View { + GeometryReader { proxy in + let rows = makeRows(availableWidth: proxy.size.width) + + VStack(alignment: .leading, spacing: verticalSpacing) { + ForEach(rows.indices, id: \.self) { rowIndex in + HStack(spacing: horizontalSpacing) { + ForEach(rows[rowIndex], id: \.self) { tag in + content(tag) + .fixedSize(horizontal: true, vertical: true) + } + } + } + } + .background( + GeometryReader { innerProxy in + Color.clear + .preference(key: HeightPreferenceKey.self, value: innerProxy.size.height) + } + ) + } + .frame(height: measuredHeight) + .onPreferenceChange(HeightPreferenceKey.self) { height in + if height != measuredHeight { + measuredHeight = height + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + + // MARK: - Layout + + private func makeRows(availableWidth: CGFloat) -> [[Tag]] { + var rows: [[Tag]] = [[]] + var currentRowWidth: CGFloat = 0 + + for tag in tags { + let tagWidth = estimatedTagWidth(tag: tag) + + if rows[rows.count - 1].isEmpty { + rows[rows.count - 1].append(tag) + currentRowWidth = tagWidth + continue + } + + let nextWidth = currentRowWidth + horizontalSpacing + tagWidth + + if nextWidth <= availableWidth { + rows[rows.count - 1].append(tag) + currentRowWidth = nextWidth + } else { + rows.append([tag]) + currentRowWidth = tagWidth + } + } + + return rows + } + + private func estimatedTagWidth(tag: Tag) -> CGFloat { + let text = String(describing: tag) + let estimatedCharacterWidth: CGFloat = 6 + let horizontalPadding: CGFloat = 16 + return CGFloat(text.count) * estimatedCharacterWidth + horizontalPadding + } +} + +// MARK: - Height Preference + +private struct HeightPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +} diff --git a/MacMagazine/WatchApp/Model/SelectedPost.swift b/MacMagazine/WatchApp/Model/SelectedPost.swift new file mode 100644 index 00000000..0acec746 --- /dev/null +++ b/MacMagazine/WatchApp/Model/SelectedPost.swift @@ -0,0 +1,14 @@ +import SwiftUI +import FeedLibrary + +// MARK: - Navigation Payload + +struct SelectedPost: Hashable, Identifiable { + let id: String + let post: FeedDB + + init(post: FeedDB) { + id = post.postId + self.post = post + } +} diff --git a/MacMagazine/WatchApp/Resources/WatchApp.swift b/MacMagazine/WatchApp/Resources/WatchApp.swift new file mode 100644 index 00000000..19eaa295 --- /dev/null +++ b/MacMagazine/WatchApp/Resources/WatchApp.swift @@ -0,0 +1,22 @@ +import FeedLibrary +import StorageLibrary +import SwiftUI + +@main +struct WatchApp: App { + + private let database = Database(models: [FeedDB.self], inMemory: false) + + var body: some Scene { + WindowGroup { + FeedRootView( + viewModel: FeedRootViewModel( + feedViewModel: FeedViewModel( + network: nil, + storage: database + ) + ) + ) + } + } +} diff --git a/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift b/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift new file mode 100644 index 00000000..567fe84a --- /dev/null +++ b/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift @@ -0,0 +1,151 @@ +import Combine +import FeedLibrary +import Foundation +import StorageLibrary +import SwiftData +import WatchKit + +@MainActor +final class FeedRootViewModel: ObservableObject { + + @Published private(set) var items: [FeedDB] = [] + @Published private(set) var status: FeedViewModel.Status = .loading + @Published var selectedIndex: Int = 0 + @Published var showActions: Bool = false + @Published var selectedPostForDetail: SelectedPost? + + private let feedViewModel: FeedViewModel + + init(feedViewModel: FeedViewModel) { + self.feedViewModel = feedViewModel + self.status = feedViewModel.status + } + + // MARK: - Public API + + func loadInitial() async { + await loadFromDB() + + if items.isEmpty { + await refresh() + } else { + status = feedViewModel.status + } + } + + func refresh() async { + _ = try? await feedViewModel.getWatchFeed() + status = feedViewModel.status + await loadFromDB() + } + + // MARK: - Private + + private func loadFromDB() async { + let context = feedViewModel.context + + let sort: [SortDescriptor] = [ + SortDescriptor(\FeedDB.pubDate, order: .reverse) + ] + + let descriptor = FetchDescriptor(sortBy: sort) + + do { + items = try context.fetch(descriptor) + } catch { + items = [] + } + + #if DEBUG + if let item = items.first { + debugPrint("item.title:", item.title) + debugPrint("item.pubDate:", item.pubDate) + debugPrint("item.artworkURL:", item.artworkURL) + debugPrint("item.link:", item.link) + } + #endif + } + + func computeSelectedIndexByMidX(items: [FeedDB], positions: [String: CGPoint]) -> Int { + guard !items.isEmpty else { return 0 } + + let screenMidX = WKInterfaceDevice.current().screenBounds.midX + + var bestIndex = 0 + var bestDistance = CGFloat.greatestFiniteMagnitude + + for (index, item) in items.enumerated() { + guard let point = positions[item.postId] else { continue } + let distance = abs(point.x - screenMidX) + if distance < bestDistance { + bestDistance = distance + bestIndex = index + } + } + + return bestIndex + } + + func computeSelectedIndexByMidY(items: [FeedDB], positions: [String: CGPoint]) -> Int { + guard !items.isEmpty else { return 0 } + + let screenMidY = WKInterfaceDevice.current().screenBounds.midY + + var bestIndex = 0 + var bestDistance = CGFloat.greatestFiniteMagnitude + + for (index, item) in items.enumerated() { + guard let point = positions[item.postId] else { continue } + let distance = abs(point.y - screenMidY) + if distance < bestDistance { + bestDistance = distance + bestIndex = index + } + } + + return bestIndex + } + + func clampIndex(_ index: Int, count: Int) -> Int { + guard count > 0 else { return 0 } + return min(max(index, 0), count - 1) + } + + func toggleActions() { + showActions.toggle() + } + + func hideActions() { + showActions = false + } + + func toggleFavorite(post: FeedDB) { + let context = feedViewModel.context + post.favorite.toggle() + + do { + try context.save() + } catch { + #if DEBUG + debugPrint("Failed to save favorite:", error.localizedDescription) + #endif + } + + showActions = true + } +} + +// MARK: - Preview Support + +extension FeedRootViewModel { + static func preview() -> FeedRootViewModel { + let database = Database(models: [FeedDB.self], inMemory: true) + let feedVM = FeedViewModel(storage: database) + let viewModel = FeedRootViewModel(feedViewModel: feedVM) + + viewModel.items = FeedDB.previewItems + viewModel.status = .done + + return viewModel + } +} diff --git a/MacMagazine/WatchApp/ViewModel/FeedScrollPositionKey.swift b/MacMagazine/WatchApp/ViewModel/FeedScrollPositionKey.swift new file mode 100644 index 00000000..56c161f1 --- /dev/null +++ b/MacMagazine/WatchApp/ViewModel/FeedScrollPositionKey.swift @@ -0,0 +1,36 @@ +import SwiftUI +import WatchKit + +struct FeedScrollPositionKey: PreferenceKey { + static var defaultValue: [String: CGPoint] = [:] + + static func reduce(value: inout [String: CGPoint], nextValue: () -> [String: CGPoint]) { + value.merge(nextValue(), uniquingKeysWith: { $1 }) + } +} + +struct FeedRowPositionReporter: ViewModifier { + let postId: String + + func body(content: Content) -> some View { + content + .background { + GeometryReader { proxy in + let frame = proxy.frame(in: .global) + let point = CGPoint(x: frame.midX, y: frame.midY) + + Color.clear + .preference( + key: FeedScrollPositionKey.self, + value: [postId: point] + ) + } + } + } +} + +extension View { + func reportFeedRowPosition(postId: String) -> some View { + modifier(FeedRowPositionReporter(postId: postId)) + } +} diff --git a/MacMagazine/WatchApp/Views/FeedDetailView.swift b/MacMagazine/WatchApp/Views/FeedDetailView.swift new file mode 100644 index 00000000..9be91d6d --- /dev/null +++ b/MacMagazine/WatchApp/Views/FeedDetailView.swift @@ -0,0 +1,109 @@ +import FeedLibrary +import SwiftUI + +struct FeedDetailView: View { + + // MARK: - Properties + + let post: FeedDB + + @StateObject private var viewModel: FeedRootViewModel + + // MARK: - Init + + init(viewModel: FeedRootViewModel, post: FeedDB) { + self.post = post + _viewModel = StateObject(wrappedValue: viewModel) + } + + // MARK: - Body + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + header + categories + bodyText + footer + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .contentShape(Rectangle()) + } + .navigationTitle("Notícia") + .navigationBarTitleDisplayMode(.inline) + } + + // MARK: - Header + + private var header: some View { + VStack(alignment: .leading, spacing: 6) { + Text(post.title) + .font(.system(size: 14)) + .bold() + .lineLimit(3) + + Text(post.dateText) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + } + } + + // MARK: - Categories (Flow Tags) + + @ViewBuilder + private var categories: some View { + if !post.categories.isEmpty { + FlowTagsView( + tags: post.categories.map { $0.uppercased() }, + horizontalSpacing: 6, + verticalSpacing: 6 + ) { tag in + Text(tag) + .font(.system(size: 8, weight: .semibold)) + .foregroundStyle(.primary) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(.secondary.opacity(0.4)) + .clipShape(Capsule()) + } + .allowsHitTesting(false) + } + } + + // MARK: - Body + + @ViewBuilder + private var bodyText: some View { + if let content = post.displayBody { + Divider() + .padding(.vertical, 4) + + Text(content) + .font(.system(size: 12)) + .multilineTextAlignment(.leading) + } + } + + // MARK: - Footer + + private var footer: some View { + VStack(spacing: 8) { + Divider() + .padding(.top, 4) + + Button { + viewModel.toggleFavorite(post: post) + } label: { + Image(systemName: post.favorite ? "star.fill" : "star") + } + .buttonStyle(.glass) + } + } +} + +// MARK: - Preview + +#Preview { + FeedDetailView(viewModel: .preview(), post: .previewItem) +} diff --git a/MacMagazine/WatchApp/Views/FeedRootView.swift b/MacMagazine/WatchApp/Views/FeedRootView.swift new file mode 100644 index 00000000..7d8616e2 --- /dev/null +++ b/MacMagazine/WatchApp/Views/FeedRootView.swift @@ -0,0 +1,197 @@ +import FeedLibrary +import SwiftUI +import WatchKit + +struct FeedRootView: View { + + // MARK: - State + + @StateObject private var viewModel: FeedRootViewModel + + // MARK: - Init + + init(viewModel: FeedRootViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + // MARK: - Body + + var body: some View { + NavigationStack { + rootContent + .navigationBarTitleDisplayMode(.inline) + .navigationTitle { + if viewModel.items.isEmpty { + Text("MacMagazine") + .font(.system(size: 12)) + } + + Text("MacMagazine\n\(viewModel.selectedIndex + 1) de \(viewModel.items.count)") + .font(.system(size: 12)) + .frame(alignment: .trailing) + .multilineTextAlignment(.trailing) + .lineLimit(2) + .offset(y: 14) + } + .refreshable { + await viewModel.refresh() + } + .task { + if viewModel.items.isEmpty { + await viewModel.loadInitial() + } + } + .navigationDestination(item: $viewModel.selectedPostForDetail) { payload in + FeedDetailView(viewModel: viewModel, post: payload.post) + } + } + } + + // MARK: - Root Content + + @ViewBuilder + private var rootContent: some View { + switch viewModel.status { + case .loading: + ProgressView("Carregando…") + + case .error(let reason): + errorView(reason: reason) + + case .done: + if viewModel.items.isEmpty { + emptyView + } else { + carouselFullScreen(items: viewModel.items) + } + } + } + + // MARK: - Full Screen Carousel + + private func carouselFullScreen(items: [FeedDB]) -> some View { + GeometryReader { geometry in + let size = geometry.size + + ZStack(alignment: .leading) { + ScrollView(.vertical, showsIndicators: false) { + LazyVStack(spacing: 0) { + ForEach(Array(items.enumerated()), id: \.element.postId) { index, post in + FeedFullScreenRowView(post: post) + .frame(width: size.width, height: size.height) + .id(index) + .reportFeedRowPosition(postId: post.postId) + } + } + .scrollTargetLayout() + } + .scrollTargetBehavior(.paging) + .scrollIndicators(.hidden) + .contentShape(Rectangle()) + .simultaneousGesture( + TapGesture().onEnded { + viewModel.toggleActions() + } + ) + .onPreferenceChange(FeedScrollPositionKey.self) { positions in + let newIndex = viewModel.computeSelectedIndexByMidY( + items: items, + positions: positions + ) + + if newIndex != viewModel.selectedIndex { + viewModel.selectedIndex = newIndex + viewModel.hideActions() + } + } + .overlay(alignment: .bottom) { + actionsOverlay(items: items) + } + + FeedDotsIndicatorView( + axis: .vertical, + count: items.count, + selectedIndex: viewModel.selectedIndex + ) + .frame(maxHeight: .infinity, alignment: .center) + .padding(.leading, 6) + .offset(x: -6) + } + } + .ignoresSafeArea() + } + + // MARK: - Actions Overlay + + private func actionsOverlay(items: [FeedDB]) -> some View { + Group { + if viewModel.showActions, let post = currentPost(items: items) { + HStack(spacing: 10) { + Button { + viewModel.toggleFavorite(post: post) + } label: { + Image(systemName: post.favorite ? "star.fill" : "star") + } + .buttonStyle(.glass) + + Button("Ver mais") { + viewModel.selectedPostForDetail = SelectedPost(post: post) + } + .buttonStyle(.glass) + } + .padding(.horizontal, 10) + .padding(.vertical, 8) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + .padding(.bottom, 10) + .transition(.opacity) + } + } + .animation(.easeInOut(duration: 0.15), value: viewModel.showActions) + } + + // MARK: - Helpers + + private func currentPost(items: [FeedDB]) -> FeedDB? { + guard !items.isEmpty else { return nil } + let index = viewModel.clampIndex(viewModel.selectedIndex, count: items.count) + return items[index] + } + + // MARK: - Shared Views + + private func errorView(reason: String) -> some View { + VStack(spacing: 10) { + Text("Não foi possível carregar.") + .font(.headline) + + Text(reason) + .font(.footnote) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + + Button("Tentar novamente") { + Task { + await viewModel.refresh() + } + } + } + .padding() + } + + private var emptyView: some View { + VStack(spacing: 8) { + Text("Sem itens") + .font(.headline) + + Text("Puxe para atualizar.") + .font(.footnote) + .foregroundStyle(.secondary) + } + } +} + +// MARK: - Preview + +#Preview("Carousel Full Screen") { + FeedRootView(viewModel: .preview()) +} diff --git a/MacMagazine/WatchApp/Views/Row/FeedFullScreenRowView.swift b/MacMagazine/WatchApp/Views/Row/FeedFullScreenRowView.swift new file mode 100644 index 00000000..73c1f776 --- /dev/null +++ b/MacMagazine/WatchApp/Views/Row/FeedFullScreenRowView.swift @@ -0,0 +1,84 @@ +import FeedLibrary +import SwiftUI + +struct FeedFullScreenRowView: View { + let post: FeedDB + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .bottom) { + background(size: geometry.size) + overlayGradient + content + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + @ViewBuilder + private func background(size: CGSize) -> some View { + ZStack { + Color.black + + if let url = post.artworkRemoteURL { + AsyncImage(url: url) { phase in + switch phase { + case .success(let image): + image + .resizable() + .scaledToFill() + .frame(width: size.width, height: size.height) + .clipped() + + case .failure: + Color.black.opacity(0.25) + + case .empty: + ProgressView() + + @unknown default: + Color.black.opacity(0.25) + } + } + } else { + Color.black.opacity(0.25) + } + } + .frame(width: size.width, height: size.height) + } + + private var overlayGradient: some View { + LinearGradient( + colors: [ + .black.opacity(0.05), + .black.opacity(0.80) + ], + startPoint: .top, + endPoint: .bottom + ) + } + + private var content: some View { + VStack(spacing: 10) { + Text(post.title) + .font(.system(size: 14)) + .fontWeight(.bold) + .foregroundStyle(.white) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(3) + .padding(.leading, 20) + + Text(post.dateText) + .font(.system(size: 10)) + .foregroundStyle(.white.opacity(0.85)) + .frame(maxWidth: .infinity, alignment: .center) + .lineLimit(1) + } + .padding(.horizontal, 8) + .padding(.bottom, 8) + } +} + +#Preview("FeedFullScreenRowView") { + FeedFullScreenRowView(post: .previewItem) +} diff --git a/MacMagazine/WatchApp/Views/Row/FeedRowView.swift b/MacMagazine/WatchApp/Views/Row/FeedRowView.swift new file mode 100644 index 00000000..e1916af4 --- /dev/null +++ b/MacMagazine/WatchApp/Views/Row/FeedRowView.swift @@ -0,0 +1,113 @@ +import FeedLibrary +import SwiftUI + +struct FeedRowView: View { + let post: FeedDB + + var body: some View { + GeometryReader { proxy in + ZStack(alignment: .bottomLeading) { + backgroundImage(proxy: proxy) + overlayGradient + content + } + .frame(width: proxy.size.width, height: rowHeight) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + } + .frame(height: rowHeight) + .padding(.horizontal, 6) + } + + // MARK: - Layout + + private var rowHeight: CGFloat { + WKInterfaceDevice.current().screenBounds.height * 0.60 + } + + // MARK: - Background + + @ViewBuilder + private func backgroundImage(proxy: GeometryProxy) -> some View { + if let url = post.artworkRemoteURL { + AsyncImage(url: url) { phase in + switch phase { + case .success(let image): + image + .resizable() + .scaledToFill() + .frame(width: proxy.size.width, height: rowHeight + parallaxExtraHeight) + .offset(y: parallaxOffset(proxy: proxy)) + .clipped() + default: + fallbackBackground + } + } + } else { + fallbackBackground + } + } + + private var fallbackBackground: some View { + Color.black.opacity(0.25) + } + + private var parallaxExtraHeight: CGFloat { + 70 + } + + private func parallaxOffset(proxy: GeometryProxy) -> CGFloat { + let screen = WKInterfaceDevice.current().screenBounds + let screenMidY = screen.midY + let cardMidY = proxy.frame(in: .global).midY + + let distance = cardMidY - screenMidY + let maxDistance = screen.height + + let progress = distance / maxDistance + let amplitude: CGFloat = 28 + + return -progress * amplitude + } + + // MARK: - Overlay + + private var overlayGradient: some View { + LinearGradient( + colors: [ + .black.opacity(0.10), + .black.opacity(0.75) + ], + startPoint: .top, + endPoint: .bottom + ) + } + + // MARK: - Content + + private var content: some View { + VStack(spacing: 10) { + Text(post.title) + .font(.system(size: 16)) + .bold() + .foregroundStyle(.white) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(3) + + Text(post.dateText) + .font(.system(size: 10)) + .foregroundStyle(.white.opacity(0.85)) + .frame(maxWidth: .infinity, alignment: .center) + .lineLimit(1) + } + .padding(.horizontal, 8) + .padding(.bottom, 40) + } +} + +#Preview("FeedRowView - Watch") { + FeedRowView(post: .previewItem) +} + +#Preview { + FeedRootView(viewModel: .preview()) +} diff --git a/MacMagazine/WatchApp/WatchApp.swift b/MacMagazine/WatchApp/WatchApp.swift deleted file mode 100644 index 725406dc..00000000 --- a/MacMagazine/WatchApp/WatchApp.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -@main -struct WatchApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} From 92deaa16d132dfcb57863d4a5deab973c2106edb Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Wed, 17 Dec 2025 18:49:29 -0300 Subject: [PATCH 2/4] Refactors Watch app UI and data loading Updates the Watch app's UI to use SwiftData for data management and simplifies data loading. - Removes ContentView and replaces it with FeedRootView. - Implements vertical scrolling carousel with FeedRowView. - Uses SwiftData to fetch and display feed items. - Improves data loading and error handling. - Moves WatchApp.swift to MainApp folder. --- MacMagazine/WatchApp/ContentView.swift | 27 ----- MacMagazine/WatchApp/Extension/FeedDB.swift | 5 + .../Helper/FeedDotsIndicatorView.swift | 32 +----- .../{Resources => MainApp}/WatchApp.swift | 5 +- .../ViewModel/FeedRootViewModel.swift | 93 ++++++---------- .../WatchApp/Views/FeedDetailView.swift | 3 +- MacMagazine/WatchApp/Views/FeedRootView.swift | 89 ++++++++++----- .../Views/Row/FeedFullScreenRowView.swift | 84 -------------- .../WatchApp/Views/Row/FeedRowView.swift | 103 +++++++----------- 9 files changed, 149 insertions(+), 292 deletions(-) delete mode 100644 MacMagazine/WatchApp/ContentView.swift rename MacMagazine/WatchApp/{Resources => MainApp}/WatchApp.swift (83%) delete mode 100644 MacMagazine/WatchApp/Views/Row/FeedFullScreenRowView.swift diff --git a/MacMagazine/WatchApp/ContentView.swift b/MacMagazine/WatchApp/ContentView.swift deleted file mode 100644 index 1df364ec..00000000 --- a/MacMagazine/WatchApp/ContentView.swift +++ /dev/null @@ -1,27 +0,0 @@ -import FeedLibrary -import StorageLibrary -import SwiftUI - -struct ContentView: View { - let viewModel = FeedViewModel( - storage: Database(models: [FeedDB.self], inMemory: true) - ) - - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") - } - .padding() - .task { - let feed = try? await viewModel.getWatchFeed() - print(feed?.map { $0.title } ?? "") - } - } -} - -#Preview { - ContentView() -} diff --git a/MacMagazine/WatchApp/Extension/FeedDB.swift b/MacMagazine/WatchApp/Extension/FeedDB.swift index c0229389..56bc4161 100644 --- a/MacMagazine/WatchApp/Extension/FeedDB.swift +++ b/MacMagazine/WatchApp/Extension/FeedDB.swift @@ -31,6 +31,10 @@ extension FeedDB { guard !artworkURL.isEmpty else { return nil } return URL(string: artworkURL) } +} + +#if DEBUG +extension FeedDB { static var previewItem: FeedDB { FeedDB( @@ -64,3 +68,4 @@ extension FeedDB { } } } +#endif diff --git a/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift b/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift index 0eae6206..a4d9dec2 100644 --- a/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift +++ b/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift @@ -1,30 +1,14 @@ import SwiftUI -enum IndicatorAxis { - case horizontal - case vertical -} - // MARK: - Dots Indicator public struct FeedDotsIndicatorView: View { - let axis: IndicatorAxis let count: Int let selectedIndex: Int public var body: some View { - Group { - switch axis { - case .vertical: - VStack(spacing: 5) { - dots - } - - case .horizontal: - HStack(spacing: 6) { - dots - } - } + VStack(spacing: 5) { + dots } .padding(6) .glassEffect(.clear) @@ -50,16 +34,12 @@ public struct FeedDotsIndicatorView: View { } } -#Preview("Vertical") { - ZStack { - Color.gray - FeedDotsIndicatorView(axis: .vertical, count: 10, selectedIndex: 3) - } -} -#Preview("Horizontal") { +#if DEBUG +#Preview { ZStack { Color.gray - FeedDotsIndicatorView(axis: .horizontal, count: 5, selectedIndex: 2) + FeedDotsIndicatorView(count: 10, selectedIndex: 3) } } +#endif diff --git a/MacMagazine/WatchApp/Resources/WatchApp.swift b/MacMagazine/WatchApp/MainApp/WatchApp.swift similarity index 83% rename from MacMagazine/WatchApp/Resources/WatchApp.swift rename to MacMagazine/WatchApp/MainApp/WatchApp.swift index 19eaa295..ba7b27b2 100644 --- a/MacMagazine/WatchApp/Resources/WatchApp.swift +++ b/MacMagazine/WatchApp/MainApp/WatchApp.swift @@ -1,22 +1,23 @@ import FeedLibrary import StorageLibrary import SwiftUI +import SwiftData @main struct WatchApp: App { - private let database = Database(models: [FeedDB.self], inMemory: false) + private let database = Database(models: [FeedDB.self], inMemory: true) var body: some Scene { WindowGroup { FeedRootView( viewModel: FeedRootViewModel( feedViewModel: FeedViewModel( - network: nil, storage: database ) ) ) + .modelContainer(database.sharedModelContainer) } } } diff --git a/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift b/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift index 567fe84a..51350b60 100644 --- a/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift +++ b/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift @@ -8,25 +8,28 @@ import WatchKit @MainActor final class FeedRootViewModel: ObservableObject { - @Published private(set) var items: [FeedDB] = [] + // MARK: - Published + @Published private(set) var status: FeedViewModel.Status = .loading @Published var selectedIndex: Int = 0 @Published var showActions: Bool = false @Published var selectedPostForDetail: SelectedPost? + // MARK: - Private + private let feedViewModel: FeedViewModel + // MARK: - Init + init(feedViewModel: FeedViewModel) { self.feedViewModel = feedViewModel - self.status = feedViewModel.status + status = feedViewModel.status } // MARK: - Public API - func loadInitial() async { - await loadFromDB() - - if items.isEmpty { + func loadInitial(hasItems: Bool) async { + if !hasItems { await refresh() } else { status = feedViewModel.status @@ -36,47 +39,37 @@ final class FeedRootViewModel: ObservableObject { func refresh() async { _ = try? await feedViewModel.getWatchFeed() status = feedViewModel.status - await loadFromDB() } - // MARK: - Private - - private func loadFromDB() async { - let context = feedViewModel.context - - let sort: [SortDescriptor] = [ - SortDescriptor(\FeedDB.pubDate, order: .reverse) - ] - - let descriptor = FetchDescriptor(sortBy: sort) + func toggleActions() { + showActions.toggle() + } - do { - items = try context.fetch(descriptor) - } catch { - items = [] - } + func hideActions() { + showActions = false + } - #if DEBUG - if let item = items.first { - debugPrint("item.title:", item.title) - debugPrint("item.pubDate:", item.pubDate) - debugPrint("item.artworkURL:", item.artworkURL) - debugPrint("item.link:", item.link) - } - #endif + func toggleFavorite(post: FeedDB) { + let context = feedViewModel.context + post.favorite.toggle() + try? context.save() + showActions = true } - func computeSelectedIndexByMidX(items: [FeedDB], positions: [String: CGPoint]) -> Int { + // MARK: - Index Calculation + + func computeSelectedIndexByMidY(items: [FeedDB], positions: [String: CGPoint]) -> Int { guard !items.isEmpty else { return 0 } - let screenMidX = WKInterfaceDevice.current().screenBounds.midX + let screenMidY = WKInterfaceDevice.current().screenBounds.midY var bestIndex = 0 var bestDistance = CGFloat.greatestFiniteMagnitude for (index, item) in items.enumerated() { guard let point = positions[item.postId] else { continue } - let distance = abs(point.x - screenMidX) + let distance = abs(point.y - screenMidY) + if distance < bestDistance { bestDistance = distance bestIndex = index @@ -86,17 +79,18 @@ final class FeedRootViewModel: ObservableObject { return bestIndex } - func computeSelectedIndexByMidY(items: [FeedDB], positions: [String: CGPoint]) -> Int { + func computeSelectedIndexByMidX(items: [FeedDB], positions: [String: CGPoint]) -> Int { guard !items.isEmpty else { return 0 } - let screenMidY = WKInterfaceDevice.current().screenBounds.midY + let screenMidX = WKInterfaceDevice.current().screenBounds.midX var bestIndex = 0 var bestDistance = CGFloat.greatestFiniteMagnitude for (index, item) in items.enumerated() { guard let point = positions[item.postId] else { continue } - let distance = abs(point.y - screenMidY) + let distance = abs(point.x - screenMidX) + if distance < bestDistance { bestDistance = distance bestIndex = index @@ -110,42 +104,21 @@ final class FeedRootViewModel: ObservableObject { guard count > 0 else { return 0 } return min(max(index, 0), count - 1) } - - func toggleActions() { - showActions.toggle() - } - - func hideActions() { - showActions = false - } - - func toggleFavorite(post: FeedDB) { - let context = feedViewModel.context - post.favorite.toggle() - - do { - try context.save() - } catch { - #if DEBUG - debugPrint("Failed to save favorite:", error.localizedDescription) - #endif - } - - showActions = true - } } + // MARK: - Preview Support +#if DEBUG extension FeedRootViewModel { static func preview() -> FeedRootViewModel { let database = Database(models: [FeedDB.self], inMemory: true) let feedVM = FeedViewModel(storage: database) let viewModel = FeedRootViewModel(feedViewModel: feedVM) - viewModel.items = FeedDB.previewItems viewModel.status = .done return viewModel } } +#endif diff --git a/MacMagazine/WatchApp/Views/FeedDetailView.swift b/MacMagazine/WatchApp/Views/FeedDetailView.swift index 9be91d6d..7d707f5f 100644 --- a/MacMagazine/WatchApp/Views/FeedDetailView.swift +++ b/MacMagazine/WatchApp/Views/FeedDetailView.swift @@ -103,7 +103,8 @@ struct FeedDetailView: View { } // MARK: - Preview - +#if DEBUG #Preview { FeedDetailView(viewModel: .preview(), post: .previewItem) } +#endif diff --git a/MacMagazine/WatchApp/Views/FeedRootView.swift b/MacMagazine/WatchApp/Views/FeedRootView.swift index 7d8616e2..718c7f73 100644 --- a/MacMagazine/WatchApp/Views/FeedRootView.swift +++ b/MacMagazine/WatchApp/Views/FeedRootView.swift @@ -1,9 +1,17 @@ import FeedLibrary +import SwiftData import SwiftUI import WatchKit struct FeedRootView: View { + // MARK: - SwiftData + + @Environment(\.modelContext) private var modelContext + + @Query(sort: \FeedDB.pubDate, order: .reverse) + private var items: [FeedDB] + // MARK: - State @StateObject private var viewModel: FeedRootViewModel @@ -21,12 +29,12 @@ struct FeedRootView: View { rootContent .navigationBarTitleDisplayMode(.inline) .navigationTitle { - if viewModel.items.isEmpty { + if items.isEmpty { Text("MacMagazine") .font(.system(size: 12)) } - Text("MacMagazine\n\(viewModel.selectedIndex + 1) de \(viewModel.items.count)") + Text("MacMagazine\n\(viewModel.selectedIndex + 1) de \(items.count)") .font(.system(size: 12)) .frame(alignment: .trailing) .multilineTextAlignment(.trailing) @@ -37,9 +45,7 @@ struct FeedRootView: View { await viewModel.refresh() } .task { - if viewModel.items.isEmpty { - await viewModel.loadInitial() - } + await viewModel.loadInitial(hasItems: !items.isEmpty) } .navigationDestination(item: $viewModel.selectedPostForDetail) { payload in FeedDetailView(viewModel: viewModel, post: payload.post) @@ -52,24 +58,24 @@ struct FeedRootView: View { @ViewBuilder private var rootContent: some View { switch viewModel.status { - case .loading: - ProgressView("Carregando…") + case .loading: + ProgressView("Carregando…") - case .error(let reason): - errorView(reason: reason) + case .error(let reason): + errorView(reason: reason) - case .done: - if viewModel.items.isEmpty { - emptyView - } else { - carouselFullScreen(items: viewModel.items) - } + case .done: + if items.isEmpty { + emptyView + } else { + carouselRowScreen(items: items) + } } } // MARK: - Full Screen Carousel - private func carouselFullScreen(items: [FeedDB]) -> some View { + private func carouselRowScreen(items: [FeedDB]) -> some View { GeometryReader { geometry in let size = geometry.size @@ -77,7 +83,7 @@ struct FeedRootView: View { ScrollView(.vertical, showsIndicators: false) { LazyVStack(spacing: 0) { ForEach(Array(items.enumerated()), id: \.element.postId) { index, post in - FeedFullScreenRowView(post: post) + FeedRowView(post: post) .frame(width: size.width, height: size.height) .id(index) .reportFeedRowPosition(postId: post.postId) @@ -109,7 +115,6 @@ struct FeedRootView: View { } FeedDotsIndicatorView( - axis: .vertical, count: items.count, selectedIndex: viewModel.selectedIndex ) @@ -179,19 +184,51 @@ struct FeedRootView: View { } private var emptyView: some View { - VStack(spacing: 8) { - Text("Sem itens") - .font(.headline) - - Text("Puxe para atualizar.") - .font(.footnote) - .foregroundStyle(.secondary) + ScrollView { + VStack(spacing: 8) { + Text("Sem itens") + .font(.headline) + + Text("Puxe para atualizar.") + .font(.footnote) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, minHeight: WKInterfaceDevice.current().screenBounds.height * 0.8) } } } // MARK: - Preview +#if DEBUG +private struct FeedRootPreviewHost: View { + + let container: ModelContainer + + init() { + let schema = Schema([FeedDB.self]) + let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) + + do { + container = try ModelContainer(for: schema, configurations: [config]) + + FeedDB.previewItems.forEach { item in + container.mainContext.insert(item) + } + + try container.mainContext.save() + } catch { + fatalError("Failed to create preview container: \(error)") + } + } + + var body: some View { + FeedRootView(viewModel: .preview()) + .modelContainer(container) + } +} + #Preview("Carousel Full Screen") { - FeedRootView(viewModel: .preview()) + FeedRootPreviewHost() } +#endif diff --git a/MacMagazine/WatchApp/Views/Row/FeedFullScreenRowView.swift b/MacMagazine/WatchApp/Views/Row/FeedFullScreenRowView.swift deleted file mode 100644 index 73c1f776..00000000 --- a/MacMagazine/WatchApp/Views/Row/FeedFullScreenRowView.swift +++ /dev/null @@ -1,84 +0,0 @@ -import FeedLibrary -import SwiftUI - -struct FeedFullScreenRowView: View { - let post: FeedDB - - var body: some View { - GeometryReader { geometry in - ZStack(alignment: .bottom) { - background(size: geometry.size) - overlayGradient - content - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - - @ViewBuilder - private func background(size: CGSize) -> some View { - ZStack { - Color.black - - if let url = post.artworkRemoteURL { - AsyncImage(url: url) { phase in - switch phase { - case .success(let image): - image - .resizable() - .scaledToFill() - .frame(width: size.width, height: size.height) - .clipped() - - case .failure: - Color.black.opacity(0.25) - - case .empty: - ProgressView() - - @unknown default: - Color.black.opacity(0.25) - } - } - } else { - Color.black.opacity(0.25) - } - } - .frame(width: size.width, height: size.height) - } - - private var overlayGradient: some View { - LinearGradient( - colors: [ - .black.opacity(0.05), - .black.opacity(0.80) - ], - startPoint: .top, - endPoint: .bottom - ) - } - - private var content: some View { - VStack(spacing: 10) { - Text(post.title) - .font(.system(size: 14)) - .fontWeight(.bold) - .foregroundStyle(.white) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(3) - .padding(.leading, 20) - - Text(post.dateText) - .font(.system(size: 10)) - .foregroundStyle(.white.opacity(0.85)) - .frame(maxWidth: .infinity, alignment: .center) - .lineLimit(1) - } - .padding(.horizontal, 8) - .padding(.bottom, 8) - } -} - -#Preview("FeedFullScreenRowView") { - FeedFullScreenRowView(post: .previewItem) -} diff --git a/MacMagazine/WatchApp/Views/Row/FeedRowView.swift b/MacMagazine/WatchApp/Views/Row/FeedRowView.swift index e1916af4..ec685b3b 100644 --- a/MacMagazine/WatchApp/Views/Row/FeedRowView.swift +++ b/MacMagazine/WatchApp/Views/Row/FeedRowView.swift @@ -5,93 +5,68 @@ struct FeedRowView: View { let post: FeedDB var body: some View { - GeometryReader { proxy in - ZStack(alignment: .bottomLeading) { - backgroundImage(proxy: proxy) + GeometryReader { geometry in + ZStack(alignment: .bottom) { + background(size: geometry.size) overlayGradient content } - .frame(width: proxy.size.width, height: rowHeight) - .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) } - .frame(height: rowHeight) - .padding(.horizontal, 6) + .frame(maxWidth: .infinity, maxHeight: .infinity) } - // MARK: - Layout - - private var rowHeight: CGFloat { - WKInterfaceDevice.current().screenBounds.height * 0.60 - } - - // MARK: - Background - @ViewBuilder - private func backgroundImage(proxy: GeometryProxy) -> some View { - if let url = post.artworkRemoteURL { - AsyncImage(url: url) { phase in - switch phase { - case .success(let image): - image - .resizable() - .scaledToFill() - .frame(width: proxy.size.width, height: rowHeight + parallaxExtraHeight) - .offset(y: parallaxOffset(proxy: proxy)) - .clipped() - default: - fallbackBackground + private func background(size: CGSize) -> some View { + ZStack { + Color.black + + if let url = post.artworkRemoteURL { + AsyncImage(url: url) { phase in + switch phase { + case .success(let image): + image + .resizable() + .scaledToFill() + .frame(width: size.width, height: size.height) + .clipped() + + case .failure: + Color.black.opacity(0.25) + + case .empty: + ProgressView() + + @unknown default: + Color.black.opacity(0.25) + } } + } else { + Color.black.opacity(0.25) } - } else { - fallbackBackground } + .frame(width: size.width, height: size.height) } - private var fallbackBackground: some View { - Color.black.opacity(0.25) - } - - private var parallaxExtraHeight: CGFloat { - 70 - } - - private func parallaxOffset(proxy: GeometryProxy) -> CGFloat { - let screen = WKInterfaceDevice.current().screenBounds - let screenMidY = screen.midY - let cardMidY = proxy.frame(in: .global).midY - - let distance = cardMidY - screenMidY - let maxDistance = screen.height - - let progress = distance / maxDistance - let amplitude: CGFloat = 28 - - return -progress * amplitude - } - - // MARK: - Overlay - private var overlayGradient: some View { LinearGradient( colors: [ - .black.opacity(0.10), - .black.opacity(0.75) + .black.opacity(0.05), + .black.opacity(0.80) ], startPoint: .top, endPoint: .bottom ) } - // MARK: - Content - private var content: some View { VStack(spacing: 10) { Text(post.title) - .font(.system(size: 16)) - .bold() + .font(.system(size: 14)) + .fontWeight(.bold) .foregroundStyle(.white) .frame(maxWidth: .infinity, alignment: .leading) .lineLimit(3) + .padding(.leading, 20) Text(post.dateText) .font(.system(size: 10)) @@ -100,14 +75,10 @@ struct FeedRowView: View { .lineLimit(1) } .padding(.horizontal, 8) - .padding(.bottom, 40) + .padding(.bottom, 8) } } -#Preview("FeedRowView - Watch") { +#Preview("FeedRowView") { FeedRowView(post: .previewItem) } - -#Preview { - FeedRootView(viewModel: .preview()) -} From 10a14905c0c4e2ae3bc005a2f805adbcd4f6aa58 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Wed, 17 Dec 2025 18:53:13 -0300 Subject: [PATCH 3/4] Resolve swiftlint --- MacMagazine/WatchApp/Model/SelectedPost.swift | 2 +- MacMagazine/WatchApp/Views/FeedRootView.swift | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/MacMagazine/WatchApp/Model/SelectedPost.swift b/MacMagazine/WatchApp/Model/SelectedPost.swift index 0acec746..ef22fa05 100644 --- a/MacMagazine/WatchApp/Model/SelectedPost.swift +++ b/MacMagazine/WatchApp/Model/SelectedPost.swift @@ -1,5 +1,5 @@ -import SwiftUI import FeedLibrary +import SwiftUI // MARK: - Navigation Payload diff --git a/MacMagazine/WatchApp/Views/FeedRootView.swift b/MacMagazine/WatchApp/Views/FeedRootView.swift index 718c7f73..4f3b5a95 100644 --- a/MacMagazine/WatchApp/Views/FeedRootView.swift +++ b/MacMagazine/WatchApp/Views/FeedRootView.swift @@ -58,18 +58,18 @@ struct FeedRootView: View { @ViewBuilder private var rootContent: some View { switch viewModel.status { - case .loading: - ProgressView("Carregando…") + case .loading: + ProgressView("Carregando…") - case .error(let reason): - errorView(reason: reason) + case .error(let reason): + errorView(reason: reason) - case .done: - if items.isEmpty { - emptyView - } else { - carouselRowScreen(items: items) - } + case .done: + if items.isEmpty { + emptyView + } else { + carouselRowScreen(items: items) + } } } From 2df6a2c1ca0609507b284190e80cf2dd6e9194f3 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Wed, 17 Dec 2025 18:57:30 -0300 Subject: [PATCH 4/4] Fixed swiftlint --- MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift | 1 - MacMagazine/WatchApp/MainApp/WatchApp.swift | 3 ++- MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift b/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift index a4d9dec2..7fd54e98 100644 --- a/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift +++ b/MacMagazine/WatchApp/Helper/FeedDotsIndicatorView.swift @@ -34,7 +34,6 @@ public struct FeedDotsIndicatorView: View { } } - #if DEBUG #Preview { ZStack { diff --git a/MacMagazine/WatchApp/MainApp/WatchApp.swift b/MacMagazine/WatchApp/MainApp/WatchApp.swift index ba7b27b2..172484f2 100644 --- a/MacMagazine/WatchApp/MainApp/WatchApp.swift +++ b/MacMagazine/WatchApp/MainApp/WatchApp.swift @@ -1,7 +1,7 @@ import FeedLibrary import StorageLibrary -import SwiftUI import SwiftData +import SwiftUI @main struct WatchApp: App { @@ -13,6 +13,7 @@ struct WatchApp: App { FeedRootView( viewModel: FeedRootViewModel( feedViewModel: FeedViewModel( + network: nil, storage: database ) ) diff --git a/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift b/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift index 51350b60..cf526f11 100644 --- a/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift +++ b/MacMagazine/WatchApp/ViewModel/FeedRootViewModel.swift @@ -106,7 +106,6 @@ final class FeedRootViewModel: ObservableObject { } } - // MARK: - Preview Support #if DEBUG