私が開発したiPhoneアプリについて、経験を元に開発手法等の基礎的な知識から具体的な手順まで、分かりやすく解説していきます。
本記事ではスレッド一覧画面からスレッド内の投稿を見る画面の作成方法について紹介します。アプリ開発はをしてみたい人の力になれましたら幸いです。
完成画面
スレッド内の投稿一覧を表示する画面になります。ここから各投稿内容を確認できます。また、ここから投稿する画面に遷移できます。
機能一覧
・CloudKitに登録されているスレッドに紐づく投稿の一覧表示機能
・投稿を新着順で表示する機能
・投稿を人気順(いいねが多い順)で表示する機能
・Firebase Realtime Databaseから取得したスレッド内の投稿数を表示する機能
・投稿内容をCloudkitから取得し、スレッドにタイトル・画像などを表示する機能
プログラム
以下が画面全体の説明になります。
import SwiftUI
import FirebaseDatabase
struct ThreadView: View {
@Environment(\.presentationMode) var presentationMode
@AppStorage("islogin") var testAppStorage = false
@AppStorage("myname") var nameAppStorage = ""
@FetchRequest(sortDescriptors: []) var manages: FetchedResults<Manage>
//遷移前の画面から取得したスレッド名
@Binding var threadname: String
//遷移前の画面から所得したスレッドに紐づく投稿数
@Binding var threadin_num: Int
//遷移前の画面から所得したスレッドに紐づく投稿数
@Binding var blocklist: [String]
//投稿の中身を開くか閉じるかの判断する変数
@State var thread_state = false
//投稿された記事の一意のIDを入れる変数
@State var id = ""
//投稿された記事のタイトルを入れる変数
@State var title = ""
//投稿された記事の本文を入れる変数
@State var report = ""
//投稿された記事の投稿主を入れる変数
@State var post_name = ""
//投稿された記事の日付を入れる変数
@State var date = Date()
//投稿された記事に対して自分がいいねを押しているか否かを判断
@State var tf = false
//投稿を新着順か人気順に並べるかを判断
@State var gt = false
//投稿された記事の画像を入れる変数(オプショナル型)
@State var selectedImaged:Data = Data.init()
//投稿数を更新するための変数
@State var count = 0
//CloudKit内の投稿一覧を格納する変数
@State var inItems = [ThreadinItem]()
//投稿するための画面に遷移するかを判断する変数
@State var isActive = false
//アラートの表示を判断する変数
@State private var showingAlert = false
@ObservedObject var db_Model : DB_Model
//インスタンスThreadinListViewModel()を指定
@StateObject var viewModel = ThreadinListViewModel()
var body: some View {
NavigationView {
//投稿する画面へ遷移するための記載
ZStack{
NavigationLink(destination: ThreadPost(threadname: $threadname, count: $count, inItems: $inItems), isActive: $isActive){
EmptyView()
}
VStack{
Text(threadname).bold().font(.system(size:24)).padding(10)
List{
//人気順に表示する
if gt {
//CloudKit内のデータをレコードごとにループ
ForEach(viewModel.items_good, id: \.self){threadin in
//ブロックしたユーザーの投稿は出さない
if blocklist.contains(threadin.name) == true {
} else {
Button(action: {
id = threadin.id
title = threadin.title
report = threadin.report
post_name = threadin.name
date = threadin.timestamp
//画像はオプショナル型のため、下記の記載方法
if threadin.imagedata != nil {
selectedImaged = threadin.imagedata
}
//関数の処理(いいねの判断)
tfManage()
//ステータスを変えて画面遷移
self.thread_state.toggle()
}){
VStack(alignment: .leading, spacing: 10) {
HStack{
Text("投稿者:" + threadin.name)
Spacer()
Text("\(threadin.good)👍")
}
HStack {
Text(threadin.title).fontWeight(.medium).font(.system(size: 18, design: .default)).fixedSize(horizontal: false, vertical: true)
Spacer()
//データが空っぽでなければ(この処理がないと、バグで画像が読み取れなかった際にアプリが止まる)
if threadin.imagedata != nil && threadin.imagedata != Data.init(){
//imagedataはbitのデータのため、ここで画像化
Image(uiImage: UIImage(data: threadin.imagedata)!).resizable().scaledToFit().frame(width: 88, height: 66)
//画像がなければ特定画像を差し込む
}else {
Image("noimage").resizable().scaledToFit().frame(width: 88, height: 66)
}
}
HStack {
Spacer()
Text("\(threadin.timestamp, formatter: itemFormatter)")
}
}.foregroundColor(Color.black)
}
}
}.listRowBackground(Color.white)
//ifの中身とほぼ同じ処理
} else {
ForEach(viewModel.items, id: \.self){threadin in
if blocklist.contains(threadin.name) == true {
} else {
Button(action: {
id = threadin.id
title = threadin.title
report = threadin.report
post_name = threadin.name
date = threadin.timestamp
if threadin.imagedata != nil {
selectedImaged = threadin.imagedata
}
tfManage()
self.thread_state.toggle()
}){
VStack(alignment: .leading, spacing: 10) {
HStack{
Text("投稿者:" + threadin.name)
Spacer()
Text("\(threadin.good)👍")
}
HStack {
Text(threadin.title).fontWeight(.medium).font(.system(size: 18, design: .default)).fixedSize(horizontal: false, vertical: true)
Spacer()
if threadin.imagedata != nil && threadin.imagedata != Data.init(){
Image(uiImage: UIImage(data: threadin.imagedata)!).resizable().scaledToFit().frame(width: 88, height: 66)
}else {
Image("noimage").resizable().scaledToFit().frame(width: 88, height: 66)
}
}
HStack {
Spacer()
Text("\(threadin.timestamp, formatter: itemFormatter)")
}
}.foregroundColor(Color.black)
}
}
}.listRowBackground(Color.white)
}
}.sheet(isPresented: $thread_state, onDismiss: {
//再度画像を格納できるようにするために初期化
selectedImaged = Data.init()
self.thread_state = false
tf = false
}){
//スレッドの中身をモーダル表示
ThreadInside(id: $id, title: $title, date:$date,selectedImaged: $selectedImaged, report: $report, post_name: $post_name,tf:$tf, threadname: $threadname,thread_state: $thread_state, db_Model : db_Model)
}
//アカウントステータスでポップアップ出したいif viewModel.canUseCloudDatabase
if viewModel.canUseCloudDatabase && nameAppStorage != "ゲスト"{
Button(action: {
inItems = viewModel.items
isActive = true
}){
Text("投稿").padding()
}
} else if nameAppStorage == "ゲスト" {
Button(action: {
self.showingAlert = true
}){
Text("投稿").padding()
}.alert(isPresented: $showingAlert) { // ③アラートの表示条件設定
Alert(title: Text("ログインが必要です。"),
message: Text("こちらの機能はログインが必須となります。"),
primaryButton: .cancel(Text("閉じる")), // キャンセル用
secondaryButton: .destructive(Text("ログイン"), action: {
testAppStorage.toggle()
}))
}
} else {
Button(action: {
self.showingAlert = true
}) {
Text("投稿").foregroundColor(Color.gray).padding()
}.alert(isPresented: $showingAlert) { // ③アラートの表示条件設定
Alert(title: Text("iCloudを使用しているAppで有効にして下さい。"),dismissButton: .default(Text("close"))) // ④アラートの定義
}
}
//ナビゲーションバーの表示部分
}.navigationBarItems(leading: HStack{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}){
Text("<戻る").padding(.horizontal, 10)
}
if gt {
Button(action: {
gt = false
}){
Text("新規順").font(.system(size:20)).padding(.leading, 35)
}
Text("人気順").font(.system(size:20)).padding(.horizontal, 15)
} else {
Text("新規順").font(.system(size:20)).padding(.leading, 35)
Button(action: {
gt = true
}){
Text("人気順").font(.system(size:20)).padding(.horizontal, 15)
}
}
},trailing:
Text("\(threadin_num + count)投稿").fixedSize(horizontal: true, vertical: false).font(.system(size:20)).frame(width: 80, height: 20))
//ここの記載をすることにより画面の更新が可能に。処理としてはDB内のデータの取得とユーザーがicloudをONにしているかの情報を取得。
.task {
await viewModel.checkAccountStatus()
await viewModel.fetchItems(threadname: threadname)
}
.redacted(reason: viewModel.isLoading ? .placeholder : [])
.refreshable {
await viewModel.fetchItems(threadname: threadname)
}
}
}.navigationViewStyle(.stack).navigationBarBackButtonHidden(true)
}
//自分が以前にタップした記事を見たことあるかの情報からその際にいいねを押したかどうかを判断
func tfManage() {
for manage in manages {
if manage.post_name == post_name && manage.title! == title {
tf = manage.good_state
}
}
}
}
//日付のフォーマットを指定
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "ja_JP")
formatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
formatter.dateFormat = "M/d(E) HH:mm"
return formatter
}()
以上がスレッド内の画面の説明になります。次は投稿の中身画面についての記事を出します。アプリ開発は初めての方にとっては難しいと思います。こ私のこの記事や他の開発に関する記事が見てくださる皆様の力になれますと幸いです。
コメント