こんにちはひまらつです。
この記事では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)
]
ここでは orders と limit を指定して最新の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("お知らせ")
// ...
}
本編とはずれますが、カテゴリに色をつけるために NewsCategory
に color
を追加します。
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をよろしくお願いいたします!