home tab with following feed in a masonry grid
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.
-
MainTabViewis the new signed-in root.ContentViewnarrows to the auth-state switch and hands off toMainTabViewas soon as a user is available. -
HomeViewrenders the/v2/illust/follow(restrict=public) feed. -
ProfileViewis the existing signed-in profile panel, extracted out ofContentView. -
IllustFeedViewModelis the@Observablefeed state. It's deliberately generic over anyPixivEndpointthat returns an illust feed — future feeds (bookmarks, search, ranking, recommended) can reuse it by constructing it with a different first-page case. -
MasonryLayoutis a realLayoutprotocol conformance.LazyVGridcan't do masonry because it normalizes row heights; this one picks the shortest current column for each subview. -
PixivImageis 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 firesloadMoreIfNeeded(reaching: illust)on appear; the view model bails unless the cell is withinprefetchDistance(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.