Develop Apps for iOSをやってみる 〜その9〜
はじめに
DX推進室の野上です。
前回 の続きです。
どこから?
今回はここから続けます。
最初に
今回も用意されたプロジェクトファイルがあります。
前回の反省を踏まえて、プロジェクトファイルを使うことにします。
データの永続化
Codableなモデルにする。
モデルをCadableにすることで、モデルをJSON化することができます。
History.swiftを開いて編集しましょう。
import Foundation
struct History: Identifiable, Codable {
let id: UUID
let date: Date
var attendees: [String]
var lengthInMinutes: Int
init(id: UUID = UUID(), date: Date = Date(), attendees: [String], lengthInMinutes: Int) {
self.id = id
self.date = date
self.attendees = attendees
self.lengthInMinutes = lengthInMinutes
}
}
DailyScrum.swiftも同じように編集します。
import SwiftUI
struct DailyScrum: Identifiable, Codable {
let id: UUID
var title: String
var attendees: [String]
var lengthInMinutes: Int
var color: Color
var history: [History]
init(id: UUID = UUID(), title: String, attendees: [String], lengthInMinutes: Int, color: Color, history: [History] = []) {
self.id = id
self.title = title
self.attendees = attendees
self.lengthInMinutes = lengthInMinutes
self.color = color
self.history = history
}
}
extension DailyScrum {
static var data: [DailyScrum] {
[
DailyScrum(title: "Design", attendees: ["Cathy", "Daisy", "Simon", "Jonathan"], lengthInMinutes: 10, color: Color("Design")),
DailyScrum(title: "App Dev", attendees: ["Katie", "Gray", "Euna", "Luis", "Darla"], lengthInMinutes: 5, color: Color("App Dev")),
DailyScrum(title: "Web Dev", attendees: ["Chella", "Chris", "Christina", "Eden", "Karla", "Lindsey", "Aga", "Chad", "Jenn", "Sarah"], lengthInMinutes: 1, color: Color("Web Dev"))
]
}
}
extension DailyScrum {
struct Data {
var title: String = ""
var attendees: [String] = []
var lengthInMinutes: Double = 5.0
var color: Color = .random
}
var data: Data {
return Data(title: title, attendees: attendees, lengthInMinutes: Double(lengthInMinutes), color: color)
}
mutating func update(from data: Data) {
title = data.title
attendees = data.attendees
lengthInMinutes = Int(data.lengthInMinutes)
color = data.color
}
}
データモデルを作成する
ScrumData.swiftというデータモデルクラスを作成しましょう。
中身はこんな感じ。
@Publishedプロパティがつく変数は、監視している側に変更があった場合通知されます。
また永続化したデータをscrums.dataという名前でdocumentDirectory配下に保存します.
import Foundation
class ScrumData: ObservableObject {
private static var documentsFolder: URL {
do {
return try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
} catch {
fatalError("Can't find documents directory.")
}
}
private static var fileURL: URL {
return documentsFolder.appendingPathComponent("scrums.data")
}
@Published var scrums: [DailyScrum] = []
}
データロードメソッドを追加する
ScrumDataクラスにload()メソッドを追加しましょう。
コードは以下のようになります。
load()メソッドはfileURLからJSONを読んできて、DailyScrum型にデコードして
@Publishedなscrumsに設定します。
load()の呼び元はscrumsを監視することで、読み出しの完了がわかります。
import Foundation
class ScrumData: ObservableObject {
private static var documentsFolder: URL {
do {
return try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
} catch {
fatalError("Can't find documents directory.")
}
}
private static var fileURL: URL {
return documentsFolder.appendingPathComponent("scrums.data")
}
@Published var scrums: [DailyScrum] = []
func load() {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let data = try? Data(contentsOf: Self.fileURL) else {
#if DEBUG
DispatchQueue.main.async {
self?.scrums = DailyScrum.data
}
#endif
return
}
guard let dailyScrums = try? JSONDecoder().decode([DailyScrum].self, from: data) else {
fatalError("Can't decode saved scrum data.")
}
DispatchQueue.main.async {
self?.scrums = dailyScrums
}
}
}
}
データセーブメソッドを追加する
次にsave()メソッドを追加します。
コードは以下のようになります。
load()の逆で、scrumsをJSONにエンコードしてfileURLに書き込みしています。
import Foundation
class ScrumData: ObservableObject {
private static var documentsFolder: URL {
do {
return try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
} catch {
fatalError("Can't find documents directory.")
}
}
private static var fileURL: URL {
return documentsFolder.appendingPathComponent("scrums.data")
}
@Published var scrums: [DailyScrum] = []
func load() {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let data = try? Data(contentsOf: Self.fileURL) else {
#if DEBUG
DispatchQueue.main.async {
self?.scrums = DailyScrum.data
}
#endif
return
}
guard let dailyScrums = try? JSONDecoder().decode([DailyScrum].self, from: data) else {
fatalError("Can't decode saved scrum data.")
}
DispatchQueue.main.async {
self?.scrums = dailyScrums
}
}
}
func save() {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let scrums = self?.scrums else { fatalError("Self out of scope") }
guard let data = try? JSONEncoder().encode(scrums) else { fatalError("Error encoding data") }
do {
let outfile = Self.fileURL
try data.write(to: outfile)
} catch {
fatalError("Can't write to file")
}
}
}
}
ロードとセーブをアプリに組み込む
実際にScrumDataをロード、セーブする処理をアプリに追加していきます。
ScrumdingerAppにまずは追加します。
import SwiftUI
@main
struct ScrumdingerApp: App {
@ObservedObject private var data = ScrumData()
var body: some Scene {
WindowGroup {
NavigationView {
ScrumsView(scrums: $data.scrums)
}
.onAppear {
data.load()
}
}
}
}
.onAppearはViewが最初に描画される際に呼び出されるコールバックメソッドです。
つまりViewが描画されたらデータのロードが開始されます。
load()が終わるとdata.scrumsに格納され、@PublishedによりScrumsViewが再描画されます。
次はScrumsViewです。
import SwiftUI
struct ScrumsView: View {
@Binding var scrums: [DailyScrum]
@Environment(\.scenePhase) private var scenePhase
@State private var isPresented = false
@State private var newScrumData = DailyScrum.Data()
let saveAction: () -> Void
var body: some View {
List {
ForEach(scrums) { scrum in
NavigationLink(destination: DetailView(scrum: binding(for: scrum))) {
CardView(scrum: scrum)
}
.listRowBackground(scrum.color)
}
}
.navigationTitle("Daily Scrums")
.navigationBarItems(trailing: Button(action: {
isPresented = true
}) {
Image(systemName: "plus")
})
.sheet(isPresented: $isPresented) {
NavigationView {
EditView(scrumData: $newScrumData)
.navigationBarItems(leading: Button("Dismiss") {
isPresented = false
}, trailing: Button("Add") {
let newScrum = DailyScrum(title: newScrumData.title, attendees: newScrumData.attendees,
lengthInMinutes: Int(newScrumData.lengthInMinutes), color: newScrumData.color)
scrums.append(newScrum)
isPresented = false
})
}
}
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}
}
private func binding(for scrum: DailyScrum) -> Binding<DailyScrum> {
guard let scrumIndex = scrums.firstIndex(where: { $0.id == scrum.id }) else {
fatalError("Can't find scrum in array")
}
return $scrums[scrumIndex]
}
}
struct ScrumsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ScrumsView(scrums: .constant(DailyScrum.data), saveAction: {})
}
}
}
Listが変更されるとsaveAction()を実行します。
saveAction()はクロージャーです。
ScrumsView_PreviewsのScrumsViewには空のクロージャーを設定しておきます。
呼び元のScrumdingerAppにsave処理を記述します。
import SwiftUI
@main
struct ScrumdingerApp: App {
@ObservedObject private var data = ScrumData()
var body: some Scene {
WindowGroup {
NavigationView {
ScrumsView(scrums: $data.scrums) {
data.save()
}
}
.onAppear {
data.load()
}
}
}
}
これで、ミーティングの中身を変更した後にアプリを終了しても、再度起動した際にデータが引き継がれているはずです。
ここまでのまとめ
- 永続化はJSONにしてファイル書き込みする方法がある
- @ObservedObjectと@Publishedを使って変更を監視できる
次回は Timer Viewの描画です。