Show HN: 原生 macOS 客户端,适用于 Hacker News,使用 SwiftUI 构建

发布: (2026年2月20日 GMT+8 22:02)
5 分钟阅读

Source: Hacker News

概览

一款基于 SwiftUI 的原生 macOS 桌面客户端,用于浏览 Hacker News,遵循 MIT 许可证开源。

  • GitHub 仓库:
  • 下载(已签名并公证的 DMG,macOS 14.0+):
  • 截图:

该应用提供 macOS 风格的体验,左侧侧边栏用于浏览故事,右侧集成阅读器用于查看文章和评论,所有功能均在单一窗口中完成。

功能

  • 分割视图布局 – 使用 NavigationSplitView,左侧侧边栏显示故事,右侧显示文章和评论。
  • 内置广告拦截 – 预编译的 WKContentRuleList 在 WebKit 层拦截 14 大广告网络(DoubleClick、Google Syndication、Criteo、Taboola、Outbrain、Amazon ads 等),可在设置中切换。
  • 弹窗拦截 – 抑制 window.open() 调用,同样可在设置中切换。
  • Hacker News 账户登录 – 完整的认证流程(登录、创建账户、密码重置)。会话存储在 macOS 钥匙串中,Cookie 注入到 WKWebView,因此可以在保持登录状态的情况下点赞、评论和提交故事。
  • 书签 – 本地保存故事以供离线访问。使用 Codable 序列化持久化,可独立搜索和过滤。
  • 搜索与过滤 – 基于 Algolia Hacker News API。可按内容类型(全部、Ask、Show、Jobs、Comments)、日期范围(今天、过去一周、过去一个月、全部时间)以及热度或最新排序进行过滤。
  • 滚动进度指示器 – 顶部的细橙色条通过 JavaScript‑to‑native 消息跟踪阅读进度。
  • 自动更新 – 使用 Sparkle,实现 EdDSA 签名的更新,更新文件托管在 GitHub Pages。
  • 暗色模式 – 通过 CSS 与 meta‑tag 注入遵循系统外观。

技术细节

架构

  • 大约 2,050 行 Swift,分布在 16 个文件中。
  • 使用现代的 @Observable 宏(取代旧的 ObservableObject/@Published 模式)。
  • 采用结构化并发,使用 async/awaitwithThrowingTaskGroup 进行并发批量获取。
  • 纯 SwiftUI 界面;唯一的 AppKit 桥接是通过 NSViewRepresentable 包装的 WKWebView

数据来源

  • 官方 Hacker News Firebase API – 获取单个条目和用户数据。
  • Algolia Search API – 提供信息流、过滤和全文搜索(日期范围过滤、分页等),这些是 Firebase API 所缺乏的。

CI/CD 流程

  • 单个 GitHub Actions 工作流(约 467 行)负责完整的 macOS 分发过程:
    1. 构建并归档应用。
    2. 使用 Developer ID 进行代码签名。
    3. 使用 Apple 进行公证(包含 5 次重试的 staple 循环,以应对票据传播延迟)。
    4. 使用 AppleScript 驱动的图标定位创建自定义 DMG。
    5. 对 DMG 进行签名并公证。
    6. 生成 EdDSA Sparkle 签名。
    7. 发布 GitHub Release。
    8. 将更新后的 appcast.xml 部署到 GitHub Pages。

在 CI 中实现 macOS 代码签名和公证是项目中最难的部分。如果你在通过 GitHub Actions 在 App Store 之外分发 macOS 应用,欢迎提问——工作流已完全开源。

许可证

整个项目遵循 MIT License。欢迎通过 Pull Request 和 Issue 进行贡献。

反馈与未来设想

我期待收到关于你希望看到的功能的反馈。正在考虑的可能增强包括:

  • 键盘驱动的导航(例如使用 j/k 在故事之间移动)。
  • 将文章简化为纯文本的阅读模式。
  • 对评论回复的通知支持。

你可以在 Hacker News 上加入讨论。

0 浏览
Back to Blog

相关文章

阅读更多 »

[SU] 形状

考虑事项 一种形状会假设其容器的框架大小。 - Rectangle.init - RoundedRectangle.initcornerRadius:style: - Circle.init - Ellipse.init - …