見出し画像

SwiftUIでQiitaのクライアントアプリを作ろう

こんにちは、まっこりです!
今回の記事では、SwiftUIを使って簡単なシングルページのQiitaのクライアントアプリを作っていきます。WebAPIとの通信の仕方、UIKitをSwiftUIの中で使う方法、MVVMのシンプルな実装方法を知ることができます。

完成時のソースコードをGitHubにアップロードしてます。必要な場合は確認してみてください。

僕は、xcodeのバージョン13.2を使用しますが、xcodeのバージョンは12でも問題ありません。

1. 新規プロジェクトの作成

まずは、新規プロジェクトの作成とフォルダ構成を作っていきます。プロジェクトテンプレートはAppを選択して、ProductNameには、QiitaClientを設定してください。また、Interfaceでは、Storyboardではなく、SwiftUIを必ず選択してください。CoreDataとTestはのチェックは外しておいてください。

プロジェクトの新規作成ができたら、フォルダ構成を作っていきます。今回はMVVMのGUIアーキテクチャを基本にして作っていこうと思うので、ModelとView、ViewModelの三つのフォルダを最初に作成しておきます。

QiitaClientフォルダの直下にView, ViewModel, Modelの三つのフォルダを追加しましょう。以下のようなフォルダ構造になっていればOKです。

QiitaClient/
    ├ View/
    ├ Model/
    ├ ViewModel/
    ├ QiitaClientApp.swift
    ├ ContentView.swift
    …(省略)

ContentView.swiftをViewフォルダーの中に移動させておきましょう。

2. Viewの作成 - 検索画面

このアプリでは2つの画面を作ります。
一つは、記事を検索する画面で、検索バーと検索結果リストを表示します。
もう一つの画面は、記事の詳細を表示する画面で、ユーザーが一つ目の画面で検索結果から記事を選択すると表示される画面です。

では、一つ目の画面の方から作っていきます。Viewフォルダの中に、SearchViewという名前のSwiftUIファイルを追加してください。

SearchViewがを追加したら、邪魔なText("Hello World")を削除して、SearchViewの骨組みを作ります。bodyの中身を以下のように書き換えてください。

struct SearchView: View {
    var body: some View {
        VStack {
            Text("サーチバーをここにおく")
            ScrollView {
                VStack {
                    // ここに検索結果のリストを置く
                }
            }
        }
    }
}

骨組みとしては以上のような形で、
今から、サーチバーのViewとリスト表示のViewを作って、この骨組みの中に埋め込んでいきます。

サーチバーの作成

では、サーチバーを作ります。Viewフォルダの中に、SearchBarという名前のSwiftUIファイルを新規作成してください。

まずは、サーチバーがもつプロパティ(Viewが持つデータ構造みたいなもの)を設定します。 サーチバーでは検索用の文字列と、検索文字列を入力中かどうかを表すブーリアンが欲しいので、そいつらを設定します。

struct SearchBar: View {
    @Binding var query: String // new 検索用文字列
    @Binding var isEditing: Bool // new 検索文字列を入力中かどうか
    
    var body: some View {
        Text("Hello, World!")
    }
}

上のようにコードを編集するとエラーが出ます。次のところでエラーが出ないようにするので、気にせず進んでください。

プロパティに@Bindingをつけると、SearchBarにプロパティを渡している親Viewとの双方向のデータのやり取りができるようになります。なので、これの場合、SearchBarのqueryが変更されると、SearchBarの親ビューであるSearchViewの方でもqueryの変更を受け取ることができます。

SwiftUIのプロパティは、結構理解するのが難しいので、わからない場合は一旦おいといておいて大丈夫です。数をこなすうちにわかるようになります。

プロパティを設定すると、SearchBar_Previewsの中のSearchBar()ところでエラーが出ると思うので、エラーが出ないように、SearchBar()にプロパティを渡してやります。

struct SearchBar_Previews: PreviewProvider {
    static var previews: some View {
        SearchBar(query: .constant("検索文字列"), isEditing: .constant(true)) // new
    }
}

ここで使っている .constantっていうのは、@Bindingのプロパティを使用しているViewをプレビューするとき専用の関数です。変更不可能な値をプレビューしたいViewに渡しています。これでエラーが消えます。

プロパティの設定とプレビューエラーの解消ができたので、SearchBarの中身を作ります。SearchBarの中身は一つのTextFieldで、コードは以下のようになります。

struct SearchBar: View {
    @Binding var query: String
    @Binding var isEditing: Bool
    
    var body: some View {
        TextField("Query", text: $query)
            .padding(.vertical, 8)
            .padding(.horizontal, 48)
            .background(Color(.systemGray4))
            .cornerRadius(8)
            .onTapGesture {
                isEditing.toggle()
            }
            .overlay(
                HStack{
                    Image(systemName: "pencil")
                        .foregroundColor(.black)
                        .frame(minWidth: 0, idealWidth: .infinity, maxWidth: .infinity, alignment: .leading)
                        .padding(.leading, 16)
                    
                    if isEditing {
                        Button {
                            isEditing = false
                            query = ""
                            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                        } label: {
                            Text("キャンセル")
                                .padding(.trailing, 16)
                        }
                    }
                }
            )
    }
}

プレビューでの見た目が下の画像のようになっていればOKです。

以下要点の説明

.onTapGesture {
     isEditing.toggle()
}

onTapGestureをTextFieldに設定することで検索フィールドをタッチするとキャンセルボタンが表示されるようになっています。

 Button {
     isEditing = false
     query = ""
     UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
 } label: {
     Text("キャンセル")
        .padding(.trailing, 16)
 }

キャンセルボタンが押されると、テキストフィールドに入力されている文字列が空になり、キーボードが非表示になります。

以上でサーチバーの作成は完了です。SearchViewの方に組み込んでいきましょう。下のコードように、SearchViewを書き換えてください。

import SwiftUI

struct SearchView: View {
    @State var query: String // new
    @State var isEditing: Bool // new
    
    var body: some View {
        VStack {
            SearchBar(query: $query, isEditing: $isEditing) // new
                .padding(8)
            ScrollView {
                VStack {
                    // ここに検索結果のリストを置く
                }
            }
        }
    }
}

struct SearchView_Previews: PreviewProvider {
    static var previews: some View {
        SearchView(query: "検索ワード", isEditing: true) //new
    }
}

要点の説明

@State var query: String
@State var isEditing: Bool

SearchBarに渡すプロパティを@Stateで宣言しています。@Stateで宣言することで、変数が変更された時に、見た目を更新することができます。

検索結果リストの作成

検索結果リストは、QiitaのAPIから得られる検索結果一件一件の情報をリストの一行にして表示します。なので、手順としてはリスト一行分のViewを作って、SearchViewにあるScrollViewの中のVstackにfor文で表示するという形になります。

リスト一行分の見た目を作ります。Viewフォルダの中にResultCell.swiftという名前でSwiftUIファイルを新規作成してください。

リスト一行の中には、記事のタイトルと著者のアイコン、著者のユーザー名を表示します。

まずは、プレビューでリスト4行分を確認できるように、ResultCell_Previewを以下のように変更してください。

struct ResultCell_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            ForEach(0...3, id: \.self) { _ in
                ResultCell()
            }
        }
        .padding()
    }
}

これで、プレビュー画面にHello Worldが4つ表示されるようになっているはずです。

そしたら、ResultCellの中身を作ります。ResultCellを以下のように変更してください。AsyncImageの中のurl部分は画像のurlであればなんでも大丈夫です。適当な画像のurlを貼ってください。

struct ResultCell: View {
    var body: some View {
        
        VStack(alignment: .leading, spacing: 10) {
            
            Text("記事タイトル")
                .font(.title2)
            
            HStack {
                AsyncImage(url: URL(string: "https://assets.st-note.com/production/uploads/images/63638586/rectangle_large_type_2_036bad6b2400148e2bab52d71576f4cc.jpg?width=800")) { image in
                    image.resizable()
                } placeholder: {
                    ProgressView()
                }
                .frame(width: 20, height: 20)
                .clipShape(Circle())
                
                Text("@username")
            }
            
            Divider()
        }
        
    }
}

プレビューに下の画像のように表示されていればOKです。

これでリスト一行分のViewが完成したので、SearchViewの方に組み込んでいきます。SearchViewのbodyを以下のように変更してください。

var body: some View {
    VStack {
        SearchBar(query: $query, isEditing: $isEditing)
            .padding(8)
        ScrollView {
            VStack {
                ForEach(0...3, id: \.self) { _ in // new
                    ResultCell() // new
                }
            }
            .padding()
        }
    }
}

プレビューで下の画像のようになるはずです。

ここまでで、SearchViewは一旦完了です。ModelとViewModelを作ってから微調整をします。

Viewとしては、記事の詳細を表示する画面もありますが、記事詳細画面のViewを作成する時に、QiitaのAPIから取得するデータの構造を定義するモデルクラスを作っておき、そのモデルクラスのインスタンスを元にデータを表示するだけのViewにしたいので、先モデルクラスを作ってしまいます。

3.モデルの作成

ここから先は

9,572字 / 4画像

¥ 250

この記事が参加している募集

つくってみた

この記事が気に入ったらサポートをしてみませんか?