自分のスキルを上げるためにQiitaのAPI(v2)を使用して記事情報を取得するiOSアプリを開発してみたいと思います。
完成図
上記画像のような検索機能を備えたアプリを開発していきます。実際には公開はしませんがソースコードはGitHubに公開しています。
環境
- Xcode 14.2
- Swift 5.7.1
- Qiita API(v2)
- MVVMアーキテクチャ
フレームワーク/ライブラリ
- Swift UI
- CocoaPods(ライブラリ管理ツール)
- Alamofire(HTTP通信)
Qiita API v2とは
Qiitaでは記事情報などをJSON形式で取得できるように「Qiita API v2」というAPI(Application Programming Interface)が用意されています。
エンドポイント
https://qiita.com/api/v2
詳しくは以下の記事を参考にしてください。
【Swift】QiitaのAPI(v2)をiOSアプリで操作する方法!パラメータやクエリの実装
開発の流れ
- 新規Xcodeプロジェクトの作成
- CocoaPodsの導入
- ライブラリ(Alamofire)の導入
- API用のModelデータ構造体の作成
- APIの取得Modelの作成
- パラメータとクエリ用のModelデータの作成
- ViewModelの作成
- リスト表示のUIを構築
- 検索機能の追加
新規Xcodeプロジェクトの作成
今回のプロジェクト名は「QiitaArticleList」にしておきました。
CocoaPodsの導入&ライブラリ(Alamofire)の導入
続いてライブラリ管理ツールである「Cocoa Pods」をプロジェクトに組み込みます。
ターミナルで以下のコマンドを打ち込み「PodFile」を生成します。
$ cd プロジェクトまでのパス
$ pod init
生成できたらPodFile内に各ライブラリをインストールするコードを追加します。
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target '(プロジェクト名)' do
use_frameworks!
pod 'Alamofire'
end
最後に実行pod install
すればプロジェクトにAlamofireが組み込まれます。
$ pod install
【Swift UI】CocoaPodsのインストール方法と使い方!
API用のModelデータ構造体の作成
続いてQiitaから取得する情報をSwift内で操作するためのModelを作成します。これはAPIで取得できるJSONデータのフォーマットに倣ったプロパティ名で定義しておきます。必要になる情報だけを保持させたいので以下のように定義しておきました。他にもAPIでいいね数や参照URLなどさまざまな情報をJSON形式で受け取れるので必要であれば構造体にプロパティを追加してください。
struct User : Decodable,Identifiable{
let id:String
let name:String
let profile_image_url:String
}
struct Article : Decodable,Identifiable{
let id:String
let title:String
let body:String
let created_at:String
let updated_at:String
let user:User
}
APIから取得できる情報は実際に記事情報を取得するJSONをみることで確認できます。
URL:https://qiita.com/api/v2/items/7da45a163e6f06eda6cf
APIの取得Modelの作成
SwiftからAPIにアクセスするために今回は「Alamofire」を使用します。対象のURLにアクセスし、レスポンスで受け取ることができるデータをJSONDecoder
クラスを使用して[構造体]
に変換します。
【Swift】Alamofireの導入と使い方!HTTP通信とAPI
import UIKit
import Alamofire
class QiitaApiModel {
private let apiUrl:String = "https://qiita.com/api/v2/items"
public func fetchArticles(params:QiitaParameterModel?,completion:@escaping ([Article]) -> Void) {
AF.request(apiUrl,method: .get,parameters: params)
.responseData { response in
do {
let decoder = JSONDecoder()
let articles = try! decoder.decode([Article].self, from: response.data!)
completion(articles)
} catch {
print(error.localizedDescription)
}
}
}
}
変換に成功した配列形式の構造体はcompletionHandler
を利用して非同期で取得できるようにしておきます。
【Swift】completionHandlerとは?使い方と@escapingの意味
パラメータとクエリ用のModelデータの作成
QiitaのAPIではURLに任意のパラメータやクエリを渡すことで取得できる記事情報をフィルタリングすることが可能です。
全記事を2個でページングした際の1ページ目
https://qiita.com/api/v2/items?page=1&per_page=2
タイトルに「swift」が含まれる記事
https://qiita.com/api/v2/items?query=title:swift
なのでパラメータやクエリを指定する用のModelも定義しておきます。
struct QiitaParameterModel: Encodable {
let page: Int
let per_page: Int
let query: String?
enum CodingKeys: String, CodingKey {
case page
case per_page
case query
}
}
enum QiitaQueryModel:String, Encodable,CaseIterable,Identifiable{
var id:String { self.rawValue }
case title // タイトルに指定の文字列が含まれる
case body // 本文に指定の文字列が含まれる
case tag // 指定のタグを持つ
case notag = "-tag" // 指定のタグを持たない
case user // 指定のユーザー
// case created // 作成日
// case updated // 更新日
case stocks // ストック数
static func getString(type:QiitaQueryModel,text:String) -> String{
return "\(type.rawValue):\(text)"
}
}
ViewModelの作成
ここまで作成したModelを操作するためのViewModelを定義します。
- ObservableObjectに準拠
- シングルトン
- @Publishedなarticlesプロパティを保持
class QiitaArticleViewModel:ObservableObject {
static let shared = QiitaArticleViewModel()
private let model = QiitaApiModel()
@Published var articles: [Article] = []
public func setArticle(params:QiitaParameterModel?){
model.fetchArticles(params: params) { array in
self.articles = array
}
}
public func getString(type:QiitaQueryModel,text:String) -> String{
QiitaQueryModel.getString(type: type, text: text)
}
}
リスト表示のUIを構築
続いて取得した記事情報をリスト形式で表示させたいため以下のようなUIをSwift UIを使用して実装していきます。
struct ListArticleView: View {
@ObservedObject var qiitaArticleVM = QiitaArticleViewModel.shared
@State var text:String = ""
@State var queryType:QiitaQueryModel = .title
var body: some View {
VStack{
QiitaHeaderView()
QiitaSearchBoxView(text: $text,queryType: $queryType)
QiitaQueryTypeButtonsView(queryType: $queryType)
if qiitaArticleVM.articles.count != 0{
List(qiitaArticleVM.articles){ article in
RowArticleView(article: article)
}.listStyle(.grouped)
}else{
Spacer()
Text("該当する記事が見つかりませんでした。")
Spacer()
}
}.onAppear{
qiitaArticleVM.setArticle(params: QiitaParameterModel(page: 1, per_page: 5,query:nil))
}
}
}
1行単位のUIは以下のように定義しています。
struct RowArticleView: View {
private let displayDateVM = DisplayDateViewModel()
let article:Article
var body: some View {
HStack{
AsyncImage(url: URL(string: article.user.profile_image_url)) { image in
image.resizable()
.cornerRadius(30)
} placeholder: {
ProgressView()
}.frame(width: 30, height: 30)
VStack{
Text(article.title)
HStack{
Spacer()
Text(displayDateVM.getQiitaFormatToDate(str: article.created_at))
}
}
}
}
}
日付情報を適切に表示させるためにDisplayDateViewModel
も定義しています。
class DisplayDateViewModel {
private let df = DateFormatter()
public func getQiitaFormatToDate(str:String) -> String{
df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
let date = df.date(from: str)!
df.dateFormat = "yyyy/MM/dd HH:mm"
return df.string(from: date)
}
}
検索機能の追加
最後に検索機能を追加します。検索機能には「タイトル」、「内容」、「タグ(含む)」、「タグ(含まない)」、「ユーザー名」、「ストック数」の6つを用意しました。これらをボタンで切り替えられるようにしておきます。また「ストック数」を選択されたときはキーボードを数字のみに変更しています。
struct QiitaSearchBoxView: View {
@ObservedObject var qiitaArticleVM = QiitaArticleViewModel.shared
@Binding var text:String
@Binding var queryType:QiitaQueryModel
@FocusState var isActive:Bool
var body: some View {
HStack{
TextField("検索文字", text: $text)
.textFieldStyle(.roundedBorder)
.padding(5)
.keyboardType(queryType == .stocks ? .numberPad : .default)
.focused($isActive)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button {
isActive = false
} label: {
Text("閉じる")
}.foregroundColor(Color(red: 121/255, green: 196/255, blue: 83/255))
}
}
.onChange(of: queryType) { newValue in
if newValue == .stocks && isActive == true{
isActive = false
isActive = true
}else if newValue != .stocks && isActive == true{
isActive = false
isActive = true
}
}
Button {
qiitaArticleVM.setArticle(params: QiitaParameterModel(page: 1, per_page: 5,query: qiitaArticleVM.getString(type: queryType ,text: text)))
} label: {
Image(systemName: "magnifyingglass")
}.padding(5)
.foregroundColor(.white)
.background(Color(red: 121/255, green: 196/255, blue: 83/255))
.cornerRadius(40)
}.padding(5)
}
}
struct QiitaQueryTypeButtonsView: View {
@Binding var queryType:QiitaQueryModel
var body: some View {
HStack{
ForEach(QiitaQueryModel.allCases) { type in
Button {
queryType = type
} label: {
Text("\(type.rawValue)")
}.padding(5)
.foregroundColor(.white)
.background(queryType == type ? Color(red: 121/255, green: 196/255, blue: 83/255):.gray)
.font(.caption)
.cornerRadius(5)
}
}
}
}
最後に
今回のソースコードはGitHubに公開しています。至らぬ部分やもっとこうした方が良いよみたいなアドバイスをいただけると嬉しいです。