私が開発したiPhoneアプリについて、経験を元に開発手法等の基礎的な知識から具体的な手順まで、分かりやすく解説していきます。
本記事ではスレッド一覧画面からスレッド内へ記事を投稿する画面の作成方法について紹介します。アプリ開発はをしてみたい人の力になれましたら幸いです。
完成画面
スレッド内へ記事を投稿する画面になります。ここからスレッドに紐づく記事を投稿できます。また、投稿はデータベースに保存され、投稿一覧に追加されます。
機能一覧
・文字を入力する機能
・画像を挿入する機能
・投稿内容のプレビューを見る機能
・CloudKitへ記事を保存する機能
・CoreDataへ自分の投稿を保存する機能
・Firebaseへスレッドに紐づく記事のIDを保存する機能
プログラム
以下が投稿画面の説明になります。
import SwiftUI
import FirebaseDatabase
struct ThreadPost: View {
//データベースへの保存処理が完了したかどうか判断
@State var tf = false
//画像データが大きい時にエラー分を格納
@State var tex = ""
@State var selectedImaged:Data = Data.init()
//写真アプリを開くかどうかを判断
@State var showphoto = false
//プレビュー画面を開くかどうかを判断
@State var thread_state = false
@State var title = ""
@State var report = ""
//ThreadinItem構造体のインスタンスを作成
@State var item = ThreadinItem(name: "", title: "", imagedata: Data.init(), report: "", threadname: "", timestamp: Date(), good: 0)
@Binding var threadname: String
//スレッド内の投稿数を前画面から引き継ぎ(投稿後+1)するため
@Binding var count: Int
//CloudKit内の投稿一覧を前画面からリストで引き継ぎ
@Binding var inItems: [ThreadinItem]
@AppStorage("myname") var nameAppStorage = ""
@Environment(\.presentationMode) var presentationMode
@Environment(\.managedObjectContext) private var viewContext
//インスタンスThreadinListViewModel()を指定
@StateObject var viewModel = ThreadinListViewModel()
let ref = Database.database().reference()
var body: some View {
NavigationView {
VStack{
//プレビューボタンを押した時の処理。thread_stateがtrueになり、プレビュー画面へ遷移
Button(action:{
self.thread_state.toggle()
}) {
Text("プレビュー").padding()
}
Form{
Section(header: Text("タイトル")){
TextField("15〜40文字",text:$title, axis: .vertical)
}
//画像が空っぽ、またはサイズが1000000Byteより大きい場合(1000000Byteより容量が大きい画像は対象外とするため)
if selectedImaged == Data.init() || selectedImaged.count > 1000000 {
Button(action: {
showphoto = true
}){
Text("画像挿入")
}.sheet(isPresented: $showphoto, onDismiss: {
showphoto = false
}){
//写真アプリを開いて画像選択する処理
ImagePicker(selectedImage: $selectedImaged, tex: $tex)
}
} else {
Button(action: {
showphoto = true
}){
Text("画像挿入済み")
}.sheet(isPresented: $showphoto, onDismiss: {
showphoto = false
}){
ImagePicker(selectedImage: $selectedImaged, tex: $tex)
}
}
Section(header: Text("記事")){
TextEditor(text: $report).frame(height: 400,alignment: .topLeading)
}
}
HStack{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}){
Text("やめる").padding()
}
//CloudKitのレコードへ格納できるサイズが1000000Byte未満であるため
if title == "" || report == "" || selectedImaged.count > 1000000{
Text("投稿").foregroundColor(Color.gray).padding()
}else {
//CloudKit、Firebase、CoreDataへデータを保存する処理
Button(action: {
addPost()
if tf {
count += 1
self.presentationMode.wrappedValue.dismiss()
}
}){
Text("投稿").padding()
}
}
}
Text(tex).font(.system(size: 13)).foregroundColor(.red)
//プレビュー画面をモーダル表示
}.sheet(isPresented: $thread_state, onDismiss: {
self.thread_state = false
}){
ThreadPreview(title: $title, report: $report, tf:$tf, selectedImaged: $selectedImaged)
}
}.navigationViewStyle(.stack).navigationBarBackButtonHidden(true)
}
//CloudKit、Firebase、CoreDataへデータを保存する処理
func addPost() {
var new_threadin = true
//スレッド内にある投稿と同じ内容であれば投稿できない。
for threadin in inItems {
if threadin.title == title && threadin.name == nameAppStorage {
tex = "以前と同じ投稿です。"
new_threadin = false
}
}
if new_threadin {
//CloudKitへ保存する処理
item = ThreadinItem(name: nameAppStorage, title: title, imagedata: selectedImaged, report: report, threadname: threadname, timestamp: Date(), good: 0)
//データを保存する処理
Task.detached {
await viewModel.saveItem(item)
}
//Firebaseへ投稿された記事のIDだけ保存
ref.child("threadin/\(threadname)/" + item.id).setValue(0)
tf = true
//CoreDataへ自分の投稿であるとわかるように投稿内容を保存
withAnimation {
let newItem = Myposts(context: viewContext)
newItem.thread_id = item.id
newItem.threadname = threadname
newItem.title = title
newItem.timestamp = Date()
//データベース保存
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
}
以下がプレビュー画面になります。
import SwiftUI
struct ThreadPreview: View {
@Environment(\.presentationMode) var presentationMode
@AppStorage("myname") var nameAppStorage = ""
//投稿画面の入力情報を引き継ぐ
@Binding var title: String
@Binding var report: String
@Binding var tf : Bool
@Binding var selectedImaged:Data
//以下は投稿内容表示画面と同じ内容
var body: some View {
NavigationView {
ScrollView {
VStack{
if selectedImaged != Data.init() {
Image(uiImage: UIImage(data: selectedImaged)!).resizable().scaledToFit().frame(width: 350.0, height: 250.0)
}else{
Image("noimage").resizable().scaledToFit().frame(width: 350.0, height: 250.0)
}
Text(title).font(.title2).bold().frame(maxWidth: .infinity, alignment: .leading).padding(EdgeInsets(top: 0, leading: 10, bottom: 2, trailing: 10))
Text("\(Date(), formatter: itemFormatter)").font(.footnote).frame(maxWidth: .infinity, alignment: .leading).foregroundColor(.gray).padding(.leading, 13)
Text(report).lineSpacing(5).frame(maxWidth: .infinity, alignment: .leading).lineSpacing(5).padding(EdgeInsets(top: 3, leading: 13, bottom: 15, trailing: 10))
Text("投稿者:" + nameAppStorage).foregroundColor(.gray).frame(maxWidth: .infinity, alignment: .trailing).padding(.trailing, 10)
Divider().frame(height: 1)
.background(Color.green)
if tf {
Button(action: {
tf = false
}){
HStack {
Spacer()
Image(systemName: "heart.fill").font(.system(size: 25)).foregroundColor(.red).padding(EdgeInsets(top: 10, leading: 0, bottom: 4, trailing: 25))
}
}
} else {
Button(action: {
tf = true
}){
HStack {
Spacer()
Image(systemName: "heart").font(.system(size: 25)).foregroundColor(.red).padding(EdgeInsets(top: 10, leading: 0, bottom: 4, trailing: 25))
}
}
}
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}){
Text("閉じる").bold()
.padding(10)
.foregroundColor(Color.white)
.background(Color.blue)
}
}.navigationBarTitle(title, displayMode: .inline).bold()
}
}.navigationViewStyle(.stack)
}
}
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
}()
以上が投稿画面と投稿内容のプレビュー画面の説明になります。次はマイページ画面についての記事を出します。アプリ開発は初めての方にとっては難しいと思います。こ私のこの記事や他の開発に関する記事が見てくださる皆様の力になれますと幸いです。
コメント