私が開発したiPhoneアプリについて、経験を元に開発手法等の基礎的な知識から具体的な手順まで、分かりやすく解説していきます。
本記事ではアプリのホーム画面に当たるスレッド一覧画面の作成方法について紹介します。アプリ開発はをしてみたい人の力になれましたら幸いです。
完成画面
スレッドの一覧が表示されている画面になります。ここから各スレッドに遷移できます。また、スレッドの作成依頼の画面に遷移します。
機能一覧
・スレッド一覧表示機能
・過去に遷移したスレッドを表示させる機能
・上位3の人気のスレッド(タップされている回数が多い)を表示する機能
・上位3の新しく作られたスレッドを表示する機能
・スレッドの作成依頼画面に遷移する機能
・スレッドを個人に紐づいた履歴をCoreDataへ保存する機能
・スレッドがタップされた回数をCloudKitへ保存する機能
・Firebase Realtime Databaseからスレッド内の投稿数を取得する機能
・ユーザーがicloudを使用可能状態かを示す機能
プログラム
ログイン状態の判断
別ファイルでログイン状態を管理し、まだログイン済みであればホーム画面を表示しています。そこを判断しているのが以下のプログラムです。(ログイン画面の前に実行しているファイル)
import SwiftUI
struct RootView: View {
@AppStorage("islogin") var testAppStorage = false
var body: some View {
if testAppStorage {
ContentView()
}else{
LoginView()
}
}
}
@AppStorage:SwiftUIでユーザーのデフォルトの永続的なストレージに簡単にアクセスするためのプロパティラッパーであり、シンプルなキー/値ペアを保存および読み取りが可能。
このプロパティは “islogin”というキーでユーザーデフォルトに保存されている。
※初期値でfalseを入れているようになっていますが、初回の起動で保存された値が存在する場合、それが優先されます。つまりtestAppStorageは起動時にユーザーデフォルトに保存されている値になります。
RootViewはエントリーポイントで起動されています。
ホーム画面
以下がホーム画面全体の説明になります。
import SwiftUI
import CoreData
import CloudKit
import Foundation
import FirebaseDatabase
struct HomeView: View {
@AppStorage("islogin") var testAppStorage = false
@AppStorage("myname") var nameAppStorage = ""
//スレッド名を格納するための変数
@State var threadname = ""
//trueで次の画面に遷移するための変数
@State var isActive = false
//スレッド作成依頼画面をモーダル表示させるための変数
@State var thread_create = false
//スレッドに投稿されている記事の数を格納するための変数
@State var threadin_num = 0
//データベースに格納されている一意のスレッドIDを格納するための変数
@State var thread_id = ""
//データベースからブロックしたユーザーを格納するための変数(格納されたユーザーの投稿は見えない)
@State private var blocklist = [String]()
@Environment(\.managedObjectContext) private var viewContext
//CoreDataからFetchedResults<xxx>のxxxで指定したデータ構造内のデータを取得し、変数に格納。「Roreki」のデータ構造からのデータの取得はtimestamp(日時)でソートされたデータを取得しています。
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Rireki.timestamp, ascending: false)],
animation: .default)
private var rirekis: FetchedResults<Rireki>
@FetchRequest(sortDescriptors: []) var blocks: FetchedResults<Block>
@ObservedObject var db_Model : DB_Model
//インスタンスThreadListViewModel()を指定
@StateObject var viewModel = ThreadListViewModel()
let ref = Database.database().reference()
var body: some View {
NavigationView {
//画面の遷移先を記載。$isActiveがtrueで遷移する。
ZStack{
NavigationLink(destination: ThreadView(threadname:$threadname, threadin_num:$threadin_num,blocklist:$blocklist, db_Model : db_Model), isActive: $isActive){
EmptyView()
}
//スレッドの一覧を表示
VStack {
List{
Menu("🔍 スレッド一覧から探す") {
ForEach(viewModel.items, id: \.self) { thread in
Button(action: {
threadname = thread.threadname
addRireki(thread_id: thread.id)
//DB内のデータを更新するための処理。クラスviewModelのメソッドupdatedb_readで処理
Task {
await viewModel.updatedb_read(threadId: thread.id)
}
ref.child("threadin/\(threadname)").observeSingleEvent(of: .value, with: { snapshot in
if let value = snapshot.value as? [String:Int] {
threadin_num = value.count
}
})
block()
isActive = true
}){
Text(thread.threadname)
}
}
}.listRowBackground(Color.white)
//自分が見たスレッドの一覧を表示
Section(header: Text("履歴")){
Menu("履歴から探す") {
ForEach(rirekis, id: \.self) { rireki in
Button(action: {
threadname = rireki.history!
thread_id = rireki.thread_id!
Task {
await viewModel.updatedb_read(threadId: thread_id)
}
//CoreDataのレコード内のデータを更新
db_Model.editRireki(item: rireki)
db_Model.writeRireki(context: viewContext)
ref.child("threadin/\(threadname)").observeSingleEvent(of: .value, with: { snapshot in
if let value = snapshot.value as? [String:Int] {
threadin_num = value.count
}
})
block()
isActive = true
}){
Text(rireki.history!)
}
}
}
}.listRowBackground(Color.white)
//スレッドの閲覧数上位3つを表示
Section(header: Text("人気ランキング")){
//注意:データベースにレコードが3つ以上なければエラーになります。
ForEach(0..<3){index in
Button(action: {
threadname = viewModel.items_read[index].threadname
addRireki(thread_id: viewModel.items_read[index].id)
Task {
do {
let myViewModel = try await viewModel // viewModelを非同期的に取得する
try await myViewModel.updatedb_read(threadId: myViewModel.items_read[index].id) // 取得されたオブジェクトのメソッドを呼び出す
} catch {
print(error.localizedDescription)
}
}
ref.child("threadin/\(threadname)").observeSingleEvent(of: .value, with: { snapshot in
if let value = snapshot.value as? [String:Int] {
threadin_num = value.count
}
})
block()
isActive = true
}){
if viewModel.items_read.count != 0 {
Text("\(index + 1)位 " + viewModel.items_read[index].threadname).foregroundColor(Color.black)
}else {
Text("-")
}
}
}
}.listRowBackground(Color.white)
//スレッドの新規作成上位3つを表示
Section(header: Text("新規")){
//注意:データベースにレコードが3つ以上なければエラーになります。
ForEach(0..<3){index in
Button(action: {
threadname = viewModel.items[index].threadname
addRireki(thread_id: viewModel.items[index].id)
Task {
do {
let myViewModel = try await viewModel // viewModelを非同期的に取得する
try await myViewModel.updatedb_read(threadId: myViewModel.items[index].id) // 取得されたオブジェクトのメソッドを呼び出す
} catch {
print(error.localizedDescription)
}
}
ref.child("threadin/\(threadname)").observeSingleEvent(of: .value, with: { snapshot in
if let value = snapshot.value as? [String:Int] {
threadin_num = value.count
}
})
block()
isActive = true
}){
if viewModel.items.count != 0 {
Text("🆕 " + viewModel.items[index].threadname).foregroundColor(Color.black)
}else {
Text("-")
}
}
}
}.listRowBackground(Color.white)
//スレッドの作成依頼のモーダル画面を出すための処理
Section {
Button(action: {
self.thread_create.toggle()
}){
HStack {
Spacer()
Text("スレッドを新規作成")
.bold()
.foregroundColor(Color.white)
Spacer()
}
}.sheet(isPresented: $thread_create, onDismiss: {
self.thread_create = false
}){
ThreadCreate()
}
}.listRowBackground(Color(.systemGreen))
}
//ナビゲーションバーの表示部分
}.navigationBarItems(leading: Text("NowShare!").font(.system(size: 30, weight: .bold, design: .monospaced)).foregroundColor(.yellow) ,trailing: HStack{
if viewModel.canUseCloudDatabase {
Image(systemName: "checkmark.icloud").font(.system(size: 22)).foregroundColor(.blue).padding(.trailing)
} else {
Image(systemName: "icloud.slash").font(.system(size: 22)).padding(.trailing)
}
}).listStyle(InsetGroupedListStyle())
//ここの記載をすることにより画面の更新が可能に。処理としてはDB内のデータの取得とユーザーがicloudをONにしているかの情報を取得。
}.task {
await viewModel.fetchItems()
await viewModel.checkAccountStatus()
}
.redacted(reason: viewModel.isLoading ? .placeholder : [])
.refreshable {
await viewModel.fetchItems()
}
}.navigationViewStyle(.stack)
}
func block() {
blocklist = []
for block in blocks {
blocklist.append(block.post_name!)
}
}
func addRireki(thread_id: String) {
var new_rireki = true
for rireki in rirekis {
if rireki.history! == threadname {
new_rireki = false
db_Model.editRireki(item: rireki)
db_Model.writeRireki(context: viewContext)
}
}
withAnimation {
if new_rireki {
let newItem = Rireki(context: viewContext)
newItem.thread_id = thread_id
newItem.history = threadname
newItem.timestamp = Date()
//データベース保存
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
}
@ObservedObject var db_Model : DB_Model
DB_Model
というクラスのインスタンスを監視するためのプロパティラッパーです。これにより、DB_Model
クラスのプロパティが変更されたときに、関連するビューが自動的に再描画(UIを更新)されます。
クラスDB_Model内の@Publishedプロパティを使用して、そのプロパティが変更されたことを検知することでUIを更新しています。今回のアプリではCoreDataのレコード内のデータを更新するために使用しています。
以上がホーム画面の説明になります。次はスレッド内の画面についての記事を出します。アプリ開発は初めての方にとっては難しいと思います。こ私のこの記事や他の開発に関する記事が見てくださる皆様の力になれますと幸いです。
コメント