はじめに
そういえば Apple の SwiftUI のチュートリアルには,
通信して取得したデータを表示する系のチュートリアルがなかったな
ということで SwiftUI の勉強会の LT 発表ネタにしようと思って
サンプルアプリを作ってみることにしました。
何を題材にしよう?と考えて,
勉強会なので connpass のイベントサーチAPI 使おうと決めました。
Enjoy SwiftUI vol2 で一部ライブコーディングした内容です。
ObservableObject と ObservedObject
SwiftUI の View 周りの実装だけやるのであれば不要な知識なのですが,
データが絡まないことは実際のアプリだとほとんどないですよね。
ちょっと敬遠してた ObservableObject
と ObservedObject
を
確認することにしました。
ObservableObject
ObservableObject
はプロトコルでこのプロトコルに準拠すると
そのクラスはイベントが発行できるようになる。
例えば ViewModel
クラスなどで使い,
値が変わったら View
クラスに通知して再描画させる感じ。
今回のサンプルアプリでは通信するクラスで使います。
@Published
プロパティラッパで,ObservableObject
に準拠したクラスで
この修飾子をつけたプロパティの値の更新を通知できる。
今回はイベントのデータモデル配列のプロパティにつけて
View
側に更新を通知しています。
@ObservedObject
こちらもプロパティラッパで ObservableObject
に準拠した,
クラスを宣言する際にプロパティにつける。
今回は,List
を実装する View
で通信クラスを宣言する際につけています。
今回作るサンプルアプリ
実装画面
実装する画面は 3画面です。
この記事ではイベントリスト画面のみ扱います。
ダークモード,Dynamic Type は対応できるようにします。
(実装してて特に意識する必要のないのが SwiftUI のいいところ!)
- 勉強会のイベントリスト画面
- イベント詳細画面(のつもり)
- 取得したイベントURLを開く WebView(SafariViewController)
top | detail | safari |
---|---|---|
実装環境
- macOS Catalina 10.15
- Xcode 11.1
- Swift 5
サンプルプロジェクト
GitHub に Push しました。気になる方はご覧ください。
https://github.com/MilanistaDev/StudyGroupEventFetcherForSwiftUI
今回使用するAPI
今回はゆめみの勉強会(YUMEMI.swift)の一覧を取得してみようと思い,
connpass のイベントサーチ API1 を用いました。
(API が充実してないなぁ・・・)
リクエストするURL
検索クエリは単純にキーワードで YUMEMI.swift で検索します。
よってリクエストする URL は下記になります。
https://connpass.com/api/v1/event/?keyword=YUMEMI.swift
レスポンスをもとにモデルを作成
API 仕様1 をもとに必要な情報だけ抜き出したものが下記になります。
{
"events": [
{
"event_url": "https://イベントのURL",
"started_at": “yyyy-mm-ddThh:mm:ss+09:00", // ISO8601 形式
"hash_tag": "Twitterのハッシュタグ",
"title": "勉強会タイトル",
"event_id": 123456,
"owner_display_name": "管理者のdisplayネーム",
"address": "住所",
},
…
]
}
よってまず events を受けるモデルを作成します。
**タップしてモデルのコードを見る**
import Foundation
struct StudyGroup: Decodable {
var events: [Event]
}
次にイベントのモデルを作成します。
**タップしてイベントモデルのコードを見る**
import Foundation
struct Event: Decodable, Identifiable {
var id: Int
var title: String
var eventUrl: String
var hashTag: String
var startDate: String
var address: String
var ownerDisplayName: String
enum CodingKeys: String, CodingKey {
case id = "event_id"
case title = "title"
case eventUrl = "event_url"
case hashTag = "hash_tag"
case startDate = "started_at"
case address = "address"
case ownerDisplayName = "owner_display_name"
}
}
モックデータとして何個かデータを用意します。
StudyGroup
モデルの中に用意しました。
**タップしてモックデータのコードを見る**
let mockEventsData: [Event]
= [Event(id: 1,
title: "YUMEMI.swift #1 ~WWDC19報告会~",
eventUrl: "https://yumemi.connpass.com/event/131175/",
hashTag: "yumemi_swift",
startDate: "2019-06-24T19:00+09:00",
address: "東京都世田谷区",
ownerDisplayName: "株式会社ゆめみ"),
Event(id: 2,
title: "YUMEMI.swift #1 ~WWDC19報告会~ パプリックビューイング @Sapporo",
eventUrl: "https://yumemi.connpass.com/event/135183/",
hashTag: "yumemi_swift",
startDate: "2019-06-24T19:00:00+09:00",
address: "北海道札幌市中央区",
ownerDisplayName: "株式会社ゆめみ"),
Event(id: 3,
title: "Enjoy SwiftUI vol1",
eventUrl: "https://yumemi.connpass.com/event/139079/",
hashTag: "yumemi_swift",
startDate: "2019-07-31T19:00:00+09:00",
address: "東京都世田谷区",
ownerDisplayName: "株式会社ゆめみ"),
Event(id: 4,
title: "YUMEMI.swift #3 ~俺/私がやったiOS 13対応~",
eventUrl: "https://yumemi.connpass.com/event/142608/",
hashTag: "yumemi_swift",
startDate: "2019-09-30T19:00:00+09:00",
address: "東京都世田谷区",
ownerDisplayName: "株式会社ゆめみ"),
Event(id: 5,
title: "Enjoy SwiftUI vol2",
eventUrl: "https://yumemi.connpass.com/event/151594/",
hashTag: "yumemi_swiftui",
startDate: "2019-11-08T19:00:00+09:00",
address: "東京都世田谷区",
ownerDisplayName: "株式会社ゆめみ"),
Event(id: 6,
title: "YUMEMI.swift #5",
eventUrl: "",
hashTag: "yumemi_swift",
startDate: "2019-11-28T19:00:00+09:00",
address: "東京都世田谷区",
ownerDisplayName: "株式会社ゆめみ")
]
実装
事前準備
用意したモックのデータを使って List
に表示するところまでやっておきます。
イベントリスト画面
List
を実装するビューを TopListView
とします。
NavigationLink
で イベント詳細画面 (EventDetailView
) に Push 遷移させています。
スタックされるビュー(セル)を EventRowView
として実装しています。
Event
型のモデルがそれぞれの画面に渡してあります。
import SwiftUI
struct TopListView: View {
let eventsData: [Event] = mockEventsData
var body: some View {
NavigationView {
List(eventsData) { event in
NavigationLink(destination: EventDetailView(eventData: event)) {
EventRowView(eventData: event)
}
}
.navigationBarTitle(Text("YUMEMI.swift一覧"))
}
}
}
プレビュー画面はこのような表示です。
セルのビュー
セル相当の実装を Extract Subview
してクラスに分けています。
シンプルに VStack
と HStack
を使ってビューを実装しています。
Event
型のモデルを受けて各ビューはそのデータを使います。
プレビューではモックのデータの最初のモデルデータを使っています。
アイコンは SF Symbols を使っています。
**タップしてセルのビューのコードを見る**
import SwiftUI
struct EventRowView: View {
var eventData: Event
var body: some View {
VStack(alignment: .leading) {
Text(eventData.title)
.bold()
.font(.headline)
.lineLimit(2)
.padding(Edge.Set.top, 8.0)
.padding(Edge.Set.bottom, 12.0)
HStack {
Image(systemName: "calendar")
.imageScale(.medium)
.foregroundColor(.red)
Text(eventData.startDate).font(.footnote)
}.padding(Edge.Set.bottom, 6.0)
HStack {
Image(systemName: "person.fill")
.imageScale(.medium)
.foregroundColor(.red)
Text(eventData.ownerDisplayName + " 他").font(.footnote)
}.padding(Edge.Set.bottom, 6.0)
HStack {
Image(systemName: "mappin.and.ellipse")
.imageScale(.medium)
.foregroundColor(.red)
Text(eventData.address)
.font(.footnote)
.lineLimit(3)
}.padding(Edge.Set.bottom, 4.0)
HStack {
Spacer()
Text("#" + eventData.hashTag)
.foregroundColor(.blue)
.font(.caption)
.padding(Edge.Set.bottom, 8.0)
}
}
}
}
struct EventRowView_Previews: PreviewProvider {
static var previews: some View {
EventRowView(eventData: mockEventsData[0])
}
}
プレビュー画面は下記のようになります。
本実装
実際に通信して取得したデータを List
に表示させます。
まずは通信する部分のコードを実装します。
今回は URLSession
を使ってみます。
SwiftUI を意識しなくていい一般的なコードです。
**タップして通信クラスのコードを見る**
import Foundation
class StudyGroupEventFetcher {
// connpass's event search API
private let urlLink = "https://connpass.com/api/v1/event/?keyword=YUMEMI.swift"
var eventData: [Event] = []
init() {
fetchEventData()
}
func fetchEventData() {
URLSession.shared.dataTask(with: URL(string: urlLink)!) { (data, response, error) in
guard let data = data else { return }
let decoder: JSONDecoder = JSONDecoder()
do {
let searchedResultData = try decoder.decode(StudyGroup.self, from: data)
DispatchQueue.main.async {
self.eventData = searchedResultData.events.reversed()
}
} catch {
print("json convert failed in JSONDecoder. " + error.localizedDescription)
}
}.resume()
}
}
次にこの通信クラスを ObservableObject
に準拠させて,
通信が終わって eventData
のプロパティに @Publushed
をつけて
取得データを代入した際にイベントを通知するように書き換えていきます。
import Foundation
class StudyGroupEventFetcher: ObservableObject {
// connpass's event search API
private let urlLink = "https://connpass.com/api/v1/event/?keyword=YUMEMI.swift"
// このプロパティに変更があった際にイベント発行
@Published var eventData: [Event] = []
init() {
fetchEventData()
}
func fetchEventData() {
URLSession.shared.dataTask(with: URL(string: urlLink)!) { (data, response, error) in
guard let data = data else { return }
let decoder: JSONDecoder = JSONDecoder()
do {
let searchedResultData = try decoder.decode(StudyGroup.self, from: data)
DispatchQueue.main.async {
self.eventData = searchedResultData.events.reversed()
}
} catch {
print("json convert failed in JSONDecoder. " + error.localizedDescription)
}
}.resume()
}
}
最後に,通知を受けてビューを更新できるようにします。
通知を受けるのは TopListView
ですね。
通信クラスを宣言して @ObservedObject をつけます。
モックデータを使っていたところを
通信クラスの eventData
のプロパティに変更します。
import SwiftUI
struct TopListView: View {
// モックのデータは使わない
// let eventsData: [Event] = mockEventsData
// ObservableObject に準拠したクラスを監視
@ObservedObject var fetcher = StudyGroupEventFetcher()
var body: some View {
NavigationView {
// 通信クラスの eventData プロパティを設定
List(fetcher.eventData) { event in
NavigationLink(destination: EventDetailView(eventData: event)) {
EventRowView(eventData: event)
}
}
.navigationBarTitle(Text("YUMEMI.swift一覧"))
}
}
}
これだけです。
結果
YUMEMI.swift で検索した結果が返ってきて List
表示できます。
ダークモードにも対応できてますね。Dynamic Type も然りです。
おまけ ~MVVM化~
ViewModel に 通知の役割を任せて MVVM にしてみます。
通信クラスを通信結果を返すだけに書き換えます。
**タップして修正した通信クラスのコードを見る**
import Foundation
class StudyGroupEventFetcher {
// connpass's event search API
private let urlLink = "https://connpass.com/api/v1/event/?keyword=YUMEMI.swift"
func fetchEventData(completion: @escaping ([Event]) -> Void) {
URLSession.shared.dataTask(with: URL(string: urlLink)!) { (data, response, error) in
guard let data = data else { return }
let decoder: JSONDecoder = JSONDecoder()
do {
let searchedResultData = try decoder.decode(StudyGroup.self, from: data)
DispatchQueue.main.async {
completion(searchedResultData.events.reversed())
}
} catch {
print("json convert failed in JSONDecoder. " + error.localizedDescription)
}
}.resume()
}
}
TopListViewModel
を作成し実装します。
このクラスで TopListView
に通知をするので
ObservableObject
に準拠させます。
ViewModel
生成時に通信するようにして結果を
@Published
をつけたプロパティに代入してイベント発行させます。
import Foundation
class TopListViewModel: ObservableObject {
let fetcher = StudyGroupEventFetcher()
@Published var eventData: [Event] = []
init() {
self.fetcher.fetchEventData { (events) in
self.eventData = events
}
}
}
最後に,TopListView
側で ViewModel
を使うようにします。
import SwiftUI
struct TopListView: View {
// モックのデータは使わない
// let eventsData: [Event] = mockEventsData
// ObservableObject に準拠したクラスを監視
// @ObservedObject var fetcher = StudyGroupEventFetcher()
// ViewModel を生成し監視する
@ObservedObject var topListVM = TopListViewModel()
var body: some View {
NavigationView {
// ViewModel の eventData プロパティを設定
List(topListVM.eventData) { event in
NavigationLink(destination: EventDetailView(eventData: event)) {
EventRowView(eventData: event)
}
}
.navigationBarTitle(Text("YUMEMI.swift一覧"))
}
}
}
動作結果は同じです。
おわりに
今回は connpass のイベントサーチ API を使って通信して
得られた勉強会イベントデータを一覧画面に表示する部分の実装について書きました。
SwiftUI での実装をしていてシンプルな画面の View を作るのは楽だけど
データが絡むと途端に難しく感じました。
プロパティラッパなど理解しながら実際にコードを使っていかないとなと。
ご覧いただきありがとうございました。
ここはこうしたほうがいい,ここはちょっと違うなどありましたらご教示いただければ幸いです。