LoginSignup
1
3

【Swift UI】Qiita API v2で記事取得アプリ(iOS)を開発する流れと実装手順

Last updated at Posted at 2023-06-27

自分のスキルを上げるためにQiitaのAPI(v2)を使用して記事情報を取得するiOSアプリを開発してみたいと思います。

完成図

スクリーンショット 2023-06-26 9.10.20.png
上記画像のような検索機能を備えたアプリを開発していきます。実際には公開はしませんがソースコードは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アプリで操作する方法!パラメータやクエリの実装

開発の流れ

  1. 新規Xcodeプロジェクトの作成
  2. CocoaPodsの導入
  3. ライブラリ(Alamofire)の導入
  4. API用のModelデータ構造体の作成
  5. APIの取得Modelの作成
  6. パラメータとクエリ用のModelデータの作成
  7. ViewModelの作成
  8. リスト表示のUIを構築
  9. 検索機能の追加

新規Xcodeプロジェクトの作成

まずはXcodeから新規プロジェクトを立ち上げます。
77FDFBF0-005C-4293-8AF8-FD7FAAEFE479.png

今回のプロジェクト名は「QiitaArticleList」にしておきました。

CocoaPodsの導入&ライブラリ(Alamofire)の導入

続いてライブラリ管理ツールである「Cocoa Pods」をプロジェクトに組み込みます。
ターミナルで以下のコマンドを打ち込み「PodFile」を生成します。

ターミナル
$ cd プロジェクトまでのパス
$ pod init

生成できたらPodFile内に各ライブラリをインストールするコードを追加します。

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形式で受け取れるので必要であれば構造体にプロパティを追加してください。

QiitaApiDataModel.swift
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

以下はURLにアクセスした際に返されるJSON。
スクリーンショット 2023-06-26 11.38.04.png

APIの取得Modelの作成

SwiftからAPIにアクセスするために今回は「Alamofire」を使用します。対象のURLにアクセスし、レスポンスで受け取ることができるデータをJSONDecoderクラスを使用して[構造体]に変換します。

【Swift】Alamofireの導入と使い方!HTTP通信とAPI

QiitaApiModel.swift
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も定義しておきます。

QiitaParameterModel.swift
struct QiitaParameterModel: Encodable {
    let page: Int
    let per_page: Int
    let query: String?
    
    enum CodingKeys: String, CodingKey {
        case page
        case per_page
        case query
    }
}

QiitaQueryModel.swift
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を定義します。

  1. ObservableObjectに準拠
  2. シングルトン
  3. @Publishedなarticlesプロパティを保持
QiitaArticleViewModel.swift
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を使用して実装していきます。

スクリーンショット 2023-06-26 19.27.08.png

ListArticleView.swift
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は以下のように定義しています。

RowArticleView.swift
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も定義しています。

DisplayDateViewModel.swift
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つを用意しました。これらをボタンで切り替えられるようにしておきます。また「ストック数」を選択されたときはキーボードを数字のみに変更しています。

スクリーンショット 2023-06-27 13.00.39.png

QiitaSearchBoxView.swift
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)
    }
}

QiitaQueryTypeButtonsView.swift
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に公開しています。至らぬ部分やもっとこうした方が良いよみたいなアドバイスをいただけると嬉しいです。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3