Skip to content

home tab with following feed in a masonry grid

jedmund requested to merge claude/home-following-feed into main

Created by: jedmund

Adds a bottom tab bar and a Home tab that shows the signed-in user's Following feed in a Pinterest-style masonry grid, with pagination as you scroll. Tapping a cell is a deliberate no-op for now.

Commits

claude.md: views layer + image loader notes
tab bar + profile tab
home view with following feed + pagination
masonry layout
pximg image loader + view
illust dtos + follow endpoint

What's new

pixillate/Views/ — a new layer for SwiftUI screens and helpers.

  • MainTabView is the new signed-in root. ContentView narrows to the auth-state switch and hands off to MainTabView as soon as a user is available.
  • HomeView renders the /v2/illust/follow (restrict=public) feed.
  • ProfileView is the existing signed-in profile panel, extracted out of ContentView.
  • IllustFeedViewModel is the @Observable feed state. It's deliberately generic over any PixivEndpoint that returns an illust feed — future feeds (bookmarks, search, ranking, recommended) can reuse it by constructing it with a different first-page case.
  • MasonryLayout is a real Layout protocol conformance. LazyVGrid can't do masonry because it normalizes row heights; this one picks the shortest current column for each subview.
  • PixivImage is the SwiftUI view for rendering a pximg URL. It reserves aspect-ratio space up front so the grid doesn't reflow when images land.

pixillate/Networking/PixivImageLoader.swift — actor-isolated loader with NSCache + single-flight dedup. Every request carries Referer: https://app-api.pixiv.net/, which the CDN gates on. AsyncImage doesn't work here because it can't set headers.

pixillate/Networking/PixivEndpoint.swift — two new cases: .followingFeed(restrict:) and .nextPage(URL). Pagination walks Pixiv's next_url through the latter, so the view model never has to build query strings itself.

pixillate/Models/PixivIllust and PixivIllustFeed DTOs. Only the fields the grid actually renders are decoded; Pixiv only ever adds keys, so extending later is cheap.

Design calls worth a review

  • Pagination trigger is cell-.task-driven. Each cell fires loadMoreIfNeeded(reaching: illust) on appear; the view model bails unless the cell is within prefetchDistance (6 by default) of the tail and there isn't already a fetch in flight. No scroll-offset math.
  • Pull-to-refresh drops state and re-runs loadInitial().
  • Empty / error states: full-screen error on first load, inline footer error on subsequent pages (grid stays on screen), placeholder when Pixiv returns zero illusts.
  • MasonryLayout is eager. All cells mount when the layout runs. That's fine for ~30 cells per page but isn't a "feed of 10k" solution — a future PR can add lazy chunking if we start hitting scroll-perf issues.

Test plan

  • Run on a simulator, sign in → land on the Home tab with a masonry grid of the Following feed.
  • Scroll down → pages append as you approach the bottom; a footer spinner shows while loading.
  • Pull to refresh → grid drops and reloads from page 1.
  • Profile tab shows user info; sign-out works and returns to the sign-in screen.
  • Kill app with a stored session → relaunch → land directly on the Home feed.
  • Cells render with a placeholder while images load, then swap in without grid reflow.

Merge request reports

Loading