📱

[SwiftUI] OSSニュースアプリを作った

4 min read

7月を通してiOS向けのニュースアプリを作りました。
OSSなのでこれからSwiftUIを学びたい人の参考になればいいなと思います。
また、API部分は別のパッケージとして切り出しているのでそちらもみていただければと。
どちらもスターくれたら嬉しいです! ⭐

https://github.com/mtfum/NewsUI

https://github.com/mtfum/NewsAPI

モチベーション

今回の開発モチベーションはiOS15から新しく使えるようになった新たな機能(Searchable, Refreshable)やSwift5.5に搭載されているasync/awaitのような言語仕様を試すことです。

そのためアプリ自体はXcode 13以上、iOS15以上(現状どちらもベータ版しかありません)が必要になります。

アプリについて

このアプリはnewsapi.orgのAPIを利用し、世界のトップニュースや特定のワードで検索ができるアプリです。
3タブ構成で必要最低限の機能だけを実装しました。
上記であげた新たな機能を試すという目的を達成できたので個人的には満足しています。

Headline Search Publishers
Headline画面 Search画面 Publishers画面

[NewsAPI]非同期処理について

アプリ作成に伴い、newsapi.orgのAPIをラップしてiOSライブラリとして公開しました。
内部実装のままでもよかったですが、

  • newsapiのSwiftライブラリで非同期処理に対応したものがなかった
  • iOSアプリを作ってみたい人に利用してもらいたい
  • OSS活動の一環としてやってみたかった

の理由からライブラリとして切り出しました。

ここからは、Swift5.5の最も大きな追加された言語機能であるasync/await構文を利用したAPIについて軽く紹介します。
下記は、NewsAPIを利用して、ヘッドラインニュースを取得するサンプルコードです。

func getHeadlines() async -> [NewsArticle] {
  do {
    let articles = try await NewsAPI(apiKey: "YOUR_API_KEY").getTopHeadlines()
    return articles
  } catch {
    // do something
  }
}

上記のコードを読むと従来のクロージャを利用したりする代わりにasyncというキーワードがメソッドに追加され、内部にawait というキーワードを利用することがわかります。
このgetHeadlines というメソッドは非同期関数として定義されているので直前にawait キーワードが必要です。
awaitは、記述直後の処理を保留するので、より直感的に記述することができるようになりました。
コード行数やネストの深さを減少させ可読性が向上するだけでなく、複数の非同期処理を同時に処理することができるようになるなどより表現できる幅の広がりを感じます。

[NewsUI]Architectureについて

ここからはアプリ本体についての説明です。
アプリの構成についてわかりやすく図にまとめたのが下記の画像です。

アプリ構成

[NewsAPI]非同期処理についてでも触れた通り、NewsAPI(赤色部分)は外部ライブラリとして切り出しました。
NewsAPIに依存するのは内部のNewsClientのみで、さらに各Featureがそれぞれ依存する形になっています。
画像の黄色部分のFeatureは全てPackageとして管理されており、Package.jsonは以下の構成になっています。

// https://github.com/mtfum/NewsUI/blob/main/Package.swift

import PackageDescription

let package = Package(
  name: "NewsUI",
  platforms: [
    .iOS("15.0")
  ],
  products: [
    .library(name: "AppFeature", targets: ["AppFeature"]),
    .library(name: "SearchFeature", targets: ["SearchFeature"]),
    .library(name: "HeadlinesFeature", targets: ["HeadlinesFeature"]),
    .library(name: "SourcesFeature", targets: ["SourcesFeature"])
  ],
  dependencies: [
    .package(url: "https://github.com/mtfum/NewsAPI.git", from: "0.1.0"),
    .package(url: "https://github.com/apple/swift-collections.git", from: "0.0.1"),
  ],
  targets: [
    .target(name: "AppComponent", dependencies: ["NewsAPI"]),
    .target(name: "AppFeature", dependencies: ["SearchFeature", "HeadlinesFeature", "SourcesFeature"]),
    .target(name: "NewsClient", dependencies: ["NewsAPI"]),
    .target(name: "SearchFeature", dependencies: ["AppComponent", "NewsClient", .product(name: "OrderedCollections", package: "swift-collections")]),
    .target(name: "HeadlinesFeature", dependencies: ["AppComponent", "NewsClient"]),
    .target(name: "SourcesFeature", dependencies: ["AppComponent", "NewsClient"])
  ]
)

アプリ内部は複数のモジュールで分けられており、各機能(今回の場合はタブごとの画面)で分割し、それぞれが相互参照できないようになっています。
マルチモジュール化はメリットとして、依存関係の明確化やコンパイルの最適化など挙げられますが今回のような小さなアプリではあまり旨味は得られないでしょう。


蛇足ですが、このアイデアをさらに突き詰めることでHyper-modularizationと呼び、86個のモジュールで開発をしているのがisowordsです。

https://github.com/pointfreeco/isowords

また、各FeatureはAppFeatureに統合され、アプリ本体はAppFeatureを参照するだけのシンプルな構成にすることができました。

import SwiftUI
import AppFeature

@main
struct NewsUIApp: App {
  var body: some Scene {
    WindowGroup {
      AppFeatureView()
    }
  }
}

おわりに

簡単ではありますが、NewsUINewsAPIについて工夫した点について紹介しました。
とりあえず一区切りついたので公開することができ、嬉しく思います。
もしも参考になったらスターください!
お読み頂きありがとうございました。

GitHubで編集を提案

Discussion

ログインするとコメントできます