microCMS

SwiftUI × microCMSでお知らせ画面をiOSアプリに実装する

ひまらつ

この記事は公開後、1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちはひまらつです。

この記事ではiOSアプリにお知らせ画面を実装していきます。

お知らせ画面はアプリの更新情報やキャンペーン、そして障害情報などをユーザーに届けられる大事な機能です。
microCMSを利用することで本体システムで障害が起きてしまった場合にもお知らせを配信し続けることができます。

今回は以下のようなお知らせ画面を実装していきます。

この記事では画面はSwiftUIで実装しますが基本的な実装はUIKitを使っている場合も同じです。

管理画面からお知らせ用のAPIを作成する

はじめに、microCMSの管理画面からお知らせ用のAPIを作成しましょう。
(まだアカウントをお持ちでない方はこちらから作成できます。)

今回は2つのAPIを作成します。「お知らせ」と「お知らせカテゴリ」です。
microCMSのコンテンツ参照の機能を使うことでカテゴリ管理をシンプルに実現できます。

お知らせカテゴリAPIを作成する

まずはAPI名エンドポイントを決めましょう。今回は「お知らせカテゴリ」、「news_categories」として進めます。

APIの型はリスト形式を選択します。

APIスキーマはカテゴリ名の1つのフィールドを定義します。


これでAPIの定義ができました。いくつかカテゴリを追加してみましょう。

今回は「使い方」「障害」「ニュース」の3つのお知らせカテゴリを作成しました。

お知らせAPIを作成する

先ほどと同様に、まずはAPI名エンドポイントを決めます。ここでは「お知らせ」、「news」とします。

APIの型はリスト形式を選択します。

APIスキーマはタイトル内容、そしてカテゴリを定義します。

カテゴリは先ほど作成したお知らせカテゴリを参照する形にしたいので、種類を「コンテンツ参照」にしています。

参照先を選択するポップアップが表示されるので「お知らせカテゴリ」を選択しましょう。

APIが作成できたらいくつかお知らせを入稿します。

今回は4つの記事を入稿してみました。

右上の「APIプレビュー」から実際のAPIのレスポンスを確認してみます。


APIレスポンスは以下のようになっていると思います。

{
    "contents": [
        {
            "id": "0e3ue4elh",
            "createdAt": "2021-09-08T04:19:49.384Z",
            "updatedAt": "2021-09-15T08:47:03.599Z",
            "publishedAt": "2021-09-08T04:19:49.384Z",
            "revisedAt": "2021-09-15T08:47:03.599Z",
            "title": "microCMS SDKでiOSアプリにバナーを実装する記事を書きました",
            "content": "<p>キャンペーンやアプリの使い方など、ユーザーにコンテンツを訴求するのにバナーは有効です。<br>バナーは画像とタップした時の遷移先URLの値で定義できますが、どこにデータを置くか困ったことはないでしょうか?<br><br>下記のブログ記事ではmicroCMSを使ってバナーをメンテナンスしやすい形で表示する実装を紹介しています。<br><a href=\"https://blog.microcms.io/ios-banner/\" target=\"_blank\" rel=\"noopener noreferrer\">https://blog.microcms.io/ios-banner/</a><br><br><strong><画面イメージ></strong><br><img src=\"https://images.microcms-assets.io/assets/9f54a34e853e4bee98b47ce18c0713f1/95653906277644da905630fc161ca095/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202021-09-08%2018.04.46.png?w=300&h=562\" alt=\"\"><br><strong><最適なサイズで表示する></strong><br>microCMSの<a href=\"https://document.microcms.io/image-api/introduction\" target=\"_blank\" rel=\"noopener noreferrer\">画像API</a>を使えば、端末に合わせて最適なサイズで画像を取得できます。<br>画像表示が高速になり、ユーザーの通信量も抑えられるのでぜひご利用ください。</p>",
            "category": {
                "id": "0t7e-qamumd",
                "createdAt": "2021-09-08T04:00:38.798Z",
                "updatedAt": "2021-09-15T08:42:58.202Z",
                "publishedAt": "2021-09-08T04:00:38.798Z",
                "revisedAt": "2021-09-15T08:42:58.202Z",
                "name": "ニュース"
            }
        },
        // ... 省略
    ],
    "totalCount": 4,
    "offset": 0,
    "limit": 10
}


途中で登場する category のレスポンスに注目してください。参照したコンテンツの中身を合わせてAPIが返してくれていることが分かります。

iOSアプリからお知らせ情報を取得する

microCMS iOS SDKの準備

microCMS iOS SDKはSwift Package Manager(SPM)より入手できます。


SPMを用いたインストールの詳しい手順は以下のドキュメントを参考にしてください。
https://document.microcms.io/tutorial/ios/ios-top

構造体の準備

先ほど定義した情報を格納するクラスを作りましょう。まずはお知らせカテゴリです。

import Foundation
import SwiftUI

struct NewsCategory {
    let id: String
    let name: String
    
    init(id: String,
         name: String) {
        self.id = id
        self.name = name
    }
    
    init?(dict: [String: Any]) {
        if let id = dict["id"] as? String,
           let name = dict["name"] as? String {
            self.id = id
            self.name = name
        } else {
            return nil
        }
    }
}


続いてお知らせです。

import Foundation

var formatter: ISO8601DateFormatter {
    let formatter = ISO8601DateFormatter()
    formatter.formatOptions.insert(.withFractionalSeconds)
    return formatter
}

struct News {
    let id: String
    let title: String
    let content: String
    let publishedAt: Date
    let category: NewsCategory
    
    init(id: String,
         title: String,
         content: String,
         publishedAt: Date,
         category: NewsCategory) {
        self.id = id
        self.title = title
        self.content = content
        self.publishedAt = publishedAt
        self.category = category
    }
    
    init?(dict: [String: Any]) {
        if let id = dict["id"] as? String,
           let title = dict["title"] as? String,
           let content = dict["content"] as? String,
           let publishedAtString = dict["publishedAt"] as? String,
           let categoryDict = dict["category"] as? [String: Any],
           let category = NewsCategory(dict: categoryDict) {
            self.id = id
            self.title = title
            self.content = content
            self.publishedAt = formatter.date(from: publishedAtString)!
            self.category = category
        } else {
            return nil
        }
    }
    
    var formattedString: String {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .short
        formatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
        formatter.locale = Locale(identifier: "ja-JP")
        return formatter.string(from: publishedAt)
    }
}


いずれも構造体の定義と、JSONオブジェクトから初期化するためのイニシャライザを用意しています。

SDKを使ってお知らせ一覧を取得する

microCMS SDKを使って定義したAPIからお知らせ情報を取得してみましょう。

NewsListView というViewを用意してお知らせのタイトル一覧を表示してみます。

import SwiftUI
import MicrocmsSDK

struct NewsListView: View {
    let client: MicrocmsClient
    
    init() {
        self.client = MicrocmsClient(
            serviceDomain: "<YOUR_SERVICE_DOMAIN>",
            apiKey: "<YOUR_API_KEY>"
        )
    }
    
    @State var news: [News] = []
    
    var body: some View {
        List {
            ForEach(news, id: \.id) { article in
                Text(article.title)
                    .padding()
            }
        }
        .navigationTitle("お知らせ")
        .navigationBarTitleDisplayMode(.inline)
        .onAppear {
            fetchNews()
        }
    }
    
    func fetchNews() {
        let params: [MicrocmsParameter] = [
            .orders(["createdAt DESC"]),
            .limit(10)
        ]
        
        client.get(
            endpoint: "news",
            params: params) { result in
            switch result {
            case .success(let object):
                if let dict = object as? [String: Any],
                   let contents = dict["contents"] as? [[String: Any]] {
                    self.news = contents.compactMap { News(dict: $0) }
                } else {
                    print("Failed to parse response")
                }
            case .failure(let error):
                print("[ERROR]: \(error)")
            }
        }
    }
}

struct NewsList_Previews: PreviewProvider {
    static var previews: some View {
        NewsListView()
    }
}


import MicrocmsSDK でSDKをインポートし、initで microCMS SDK を初期化します。

  • serviceDomain に https://xxxxx.microcms.io/ の xxxxx にあたる部分をセット
  • apiKey はダッシュボードから X-API-KEY の値をセット


画面表示時に記事の一覧を取得するメソッド fetchNews() を呼んでいます。

パラメータの指定

リクエスト部分ではクエリパラメータを指定しています。

let params: [MicrocmsParameter] = [
    .orders(["createdAt DESC"]),
    .limit(10)
]


ここでは orderslimit を指定して最新の10件のお知らせを取得しています。
パラメータは他にも指定フィールドだけを取得する fields、全文検索のための q 、より複雑な条件を記述できる filter などがあります。
microCMSで利用できるすべてのパラメータはこちらのドキュメントでご確認ください。

NewsListView が起動時に表示されるように変更しましょう。ContentView を次のように修正します。

import SwiftUI

@main
struct ContentView: View {
    var body: some View {
        NavigationView {
            NewsListView()
        }
    }
}

アプリを起動し、お知らせのタイトルの一覧が画面に表示されていれば成功です。

カテゴリ情報と日付を表示する

タイトル表示だけではお知らせの新鮮度やカテゴリがわかりません。もう少し情報を追加してわかりやすくしてみましょう。

先ほど Text を表示していた部分を以下の実装に変更します。

struct NewsListView: View {
    // ....
    var body: some View {
        List {
            ForEach(news, id: \.id) { article in
                // ----- ここから変更
                VStack(alignment: .leading, spacing: 2) {
                    Text(article.category.name)
                        .font(.system(size: 14))
                        .bold()
                        .foregroundColor(article.category.color)
                        .padding(.bottom, 6)
                    Text(article.title)
                        .padding(.bottom, 4)
                    
                    HStack {
                        Spacer()
                        
                        Text(article.formattedPublishedAt)
                            .font(.system(size: 12))
                            .lineLimit(1)
                            .foregroundColor(.gray)
                    }
                }
                .padding(EdgeInsets(top: 8, leading: 4, bottom: 8, trailing: 4))
                // ---- ここまで
            }
        }
        .navigationTitle("お知らせ")
        // ...
}


本編とはずれますが、カテゴリに色をつけるために NewsCategorycolor を追加します。

struct NewsCategory {
    // ...    
    var color: Color {
        if name == "障害" {
            return .red
        } else if name == "使い方" {
            return .green
        } else {
            return .blue
        }
    }
}


アプリを起動すると以下のようになっているはずです。カテゴリと日時が表示されてわかりやすくなりました。

お知らせ詳細画面を作成する

詳細な内容を表示するお知らせ詳細画面を作成し、リストをタップした時に遷移させてみます。

まずは以下の内容で NewsDetailView を作成します。

import SwiftUI

struct NewsDetailView: View {
    let news: News
    
    var body: some View {
        VStack {
            Text(news.content)            
            Spacer()
        }
        .padding()
    }
}


次に、NewsListView を以下のように修正してタップ時に遷移するように変更します。

var body: some View {
        List {
            ForEach(news, id: \.id) { article in
                NavigationLink(
                    destination: NewsDetailView(news: article),
                    label: {
                        VStack(alignment: .leading, spacing: 2) {
                            // ...
                        }
                        .padding(EdgeInsets(top: 8, leading: 4, bottom: 8, trailing: 4))
                    })
            }
        }
        // ...
}


表示コンテンツをNavigationLinkでラップし、タップ時に NewsDetailView に遷移させています。

詳細画面は以下のように表示されます。

お知らせの内容は表示できましたが、HTMLタグがそのまま表示されていますね。
microCMSのリッチエディタで入稿したテキストはこのようなHTML構造のテキストになっており、そのまま Text で表示することはできません。

対応方針はいくつか考えられます。

  • テキストからHTMLタグを除いて平文として表示する
  • microCMSの入稿のスキーマを「リッチエディタ」から「テキストエリア」に変更する
  • WebViewでHTMLを表示する


今回は3つ目のWebViewを使った方法で実装していきます。

WebViewを使ってリッチエディタで入稿した内容を表示する

SwiftUIではWebViewはまだサポートされていないため、UIKitの世界から持ってくる必要があります。
WebView というファイルを作成し、以下を記述しましょう。

import UIKit
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let text: String
    private let webView = WKWebView()
    
    @Binding var dynamicHeight: CGFloat
    
    func makeUIView(context: Context) -> WKWebView {
        webView.navigationDelegate = context.coordinator
        webView.scrollView.bounces = false
        
        let htmlStart = """
        <HTML><HEAD><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\">
            <style>
                body {
                    line-height: 1.5;
                    font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", YuGothic, "ヒラギノ角ゴ ProN W3", Hiragino Kaku Gothic ProN, Arial, "メイリオ", Meiryo, sans-serif;
                    font-size: 18px;
                    padding: 12px;
                }
                ul > li {
                    line-height: 1.8;
                }
                
            </style>
            </HEAD><BODY>
        """
        let htmlEnd = "</BODY></HTML>"
        let html = htmlStart + text + htmlEnd
        webView.loadHTMLString(html, baseURL: nil)
        return webView
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: WebView

        init(_ parent: WebView) {
            self.parent = parent
        }

        // 表示できたらWebViewのコンテンツのサイズを計算する
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in
                DispatchQueue.main.async {
                    self.parent.dynamicHeight = height as! CGFloat
                }
            })
        }
        
        // HTMLリンクをタップした時にSafariで開くようにしている
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
            if navigationAction.navigationType == .linkActivated  {
                if let url = navigationAction.request.url,
                   UIApplication.shared.canOpenURL(url) {
                    UIApplication.shared.open(url)
                    decisionHandler(.cancel)
                } else {
                    decisionHandler(.allow)
                }
            } else {
                decisionHandler(.allow)
            }
        }
    }
}


長く処理が書かれていますが、重要なのは makeUIView(context: Context) の部分です。
このメソッドではmicroCMSで取得したHTMLに <BODY> タグや CSS を追加して整形しています。整形されたHTMLをwebViewのloadHTMLString()に渡し、画面にレンダリングします。

このWebViewを使って先ほどの NewsDetailView を以下のように変更します。

var body: some View {
    ScrollView {
        VStack(alignment: .leading) {
            Text(news.title)
                .font(.title)
                .bold()
                .padding(EdgeInsets(top: 20, leading: 16, bottom: 8, trailing: 16))

            Text(news.category.name)
                .font(.subheadline)
                .foregroundColor(news.category.color)
                .padding(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
                .overlay(
                    RoundedRectangle(cornerRadius: 4)
                        .stroke(news.category.color, lineWidth: 1)
                )
                .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))

            WebView(text: news.content,
                    dynamicHeight: $webViewHeight)
                .frame(height: webViewHeight)
        }
    }
}


WebViewでお知らせの内容を表示し、加えてタイトルやカテゴリの情報を追加してみました。
アプリを実行してみましょう。

入稿した通りにお知らせを表示できました。

カテゴリでフィルタしたお知らせを表示する

最後に、お知らせをフィルタしてみましょう。

詳細画面のカテゴリをタップするとそのカテゴリのお知らせだけを表示してみます。

まず、ニュース一覧を表示している NewsListView にカテゴリを渡せるように変更します。

struct NewsListView: View {
    let client: MicrocmsClient
    
    // ここを追加
    let filteredCategory: NewsCategory?
    
    init(category: NewsCategory? = nil) {
        self.client = MicrocmsClient(
            serviceDomain: "microcms-app-sample",
            apiKey: "bfc589b9-63be-40df-8935-3654ec277789"
        )

        // ここを追加
        self.filteredCategory = category
    }
    
    func fetchNews() {
        var params: [MicrocmsParameter] = [
            .orders(["createdAt DESC"]),
            .limit(10)
        ]
    
        // ここを追加。カテゴリがあればフィルタする    
        if let filteredCategory = filteredCategory {
            params.append(.filters("category[equals]\(filteredCategory.id)"))
        }
        
        client.get(
            // ...


filteredCategory という変数を追加し、これがあればカテゴリを絞り込むようにしています。

if let filteredCategory = filteredCategory {
    params.append(.filters("category[equals]\(filteredCategory.id)"))
}

filters に渡すパラメータはmicroCMSのAPIプレビューで調整するのがおすすめです。


実際のレスポンスを確認しながらパラメータを調整できます。iOS SDKを使った場合のコードサンプルも表示されるのでクイックに実装できます。

最後に、詳細画面のカテゴリをタップした時の画面遷移を実装しましょう。

struct NewsDetailView: View {
    // ...
    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                // カテゴリがタップされたら画面遷移する
                NavigationLink(destination: NewsListView(category: news.category)) {
                    Text(news.category.name)
                        // ...
                }
                // ...
            }
        }
    }
}


カテゴリを表示している Text を NavigationLink で包み、遷移先として NewsListView を指定します。遷移先ではカテゴリで絞りたいので news.category を渡しています。

アプリを起動して確認してみましょう。

「ニュース」カテゴリのお知らせだけが表示されました。microCMSで管理することでこのようなフィルタも簡単かつ柔軟に表現できます。

環境

本記事は以下のバージョンで確認しています。バージョンの差異によって若干機能が異なる可能性があります。

  • Xcode 12.5
  • Swift 5.4


おわりに

iOSアプリにお知らせ機能を実装する方法を紹介しました。microCMSでお知らせを管理することで、入稿しやすくメンテナブルなお知らせ機能を作ることができます。この記事がご参考になれば嬉しいです。


-----

microCMSは日々改善を進めています。
ご意見・ご要望は管理画面右下のチャット、公式Twitterメールからお気軽にご連絡ください!
引き続きmicroCMSをよろしくお願いいたします!

まずは、無料で試してみましょう。

APIベースの日本製ヘッドレスCMS「microCMS」を使えば、 ものの数分でAPIの作成ができます。

microCMSを無料で始める

microCMSについてお問い合わせ

初期費用無料・14日間の無料トライアル付き。ご不明な点はお気軽にお問い合わせください。

お問い合わせ

microCMS公式アカウント

microCMSは各公式アカウントで最新情報をお届けしています。
フォローよろしくお願いします。

  • X
  • Discord
  • github

ABOUT ME

ひまらつ
SwiftやPythonやスプラトゥーンを楽しんでます