【情報共有アプリ】マイページ画面

App

私が開発したiPhoneアプリについて、経験を元に開発手法等の基礎的な知識から具体的な手順まで、分かりやすく解説していきます。

本記事ではマイページ画面の作成方法について紹介します。アプリ開発はをしてみたい人の力になれましたら幸いです。

完成画面

マイページを表示する画面になります。画面の動きを説明します。
– タブでマイページに遷移
– 自分の投稿にいいねを押し、マイページ上でいいねが1→2に変わる
– いいね一覧から過去に自分がいいねした投稿の一覧を表示
– 自分の投稿から自分が投稿した記事を表示する。特定記事を左にスワイプし、投稿を削除
– プロックしたユーザーより、ブロック済みのユーザーを表示。
– 名前の変更より「やまだたろう」→「ナスジニア」へ名前を変更
– アカウントを削除し、ログイン画面へ戻る

機能一覧

・タブでホームとマイページを切り替える機能
・投稿にいいねされた数を表示する機能
・自分がいいねした投稿を一覧として保存し、表示できる機能
・自分の投稿を一覧として保存し、表示できる機能
・ブロックしたユーザーを表示し、ブロックを解除できる機能
・名前を変更する機能
・アカウントを削除する機能(再度ログイン画面に遷移)

プログラム

以下がマイページ画面全体の説明になります。

import SwiftUI
import FirebaseDatabase

struct MypageView: View {
    //合計いいね数を格納
    @State var good_sum = 0
    //trueでいいね一覧画面を表示
    @State var status_good = false
    //trueで名前変更画面を表示
    @State var status_name = false
    //trueで投稿一覧画面を表示
    @State var status_post = false
    //trueでブロックユーザー一覧画面を表示
    @State var status_block = false
    //trueでアラート画面を表示(アカウント削除に対して)
    @State var showingAlert = false

    //ログイン状態を格納
    @AppStorage("islogin") var testAppStorage = false
    @AppStorage("myname") var nameAppStorage = ""
    
    //CoreDataにあるアカウント情報を取得
    @FetchRequest(sortDescriptors: []) var persons: FetchedResults<Person>
    @Environment(\.managedObjectContext) private var viewContext

    @StateObject var viewModel = ThreadinListViewModel()
    @ObservedObject var db_Model : DB_Model

    var body: some View {
        NavigationView {
            VStack {
                List{
                    HStack {
                        Image(systemName: "person.circle").font(.system(size: 20)).foregroundColor(Color.black)
                        Text(nameAppStorage).foregroundColor(Color.black)
                    }.listRowBackground(Color.white)
                    HStack {
                        Text("合計いいね数: ").foregroundColor(Color.black)
                        Text("\(good_sum)").bold().foregroundColor(Color.black)
                    }.listRowBackground(Color.white)
                    Section(header: Text("設定")){
                        //いいね一覧画面を表示させる処理
                        Button(action: {
                            status_good = true
                        }){
                            Text("いいね一覧").foregroundColor(Color.black)
                        }.sheet(isPresented: $status_good, onDismiss: {
                            status_good = false
                        }){
                            ManageView(db_Model : db_Model)
                        }
                        //自分の投稿一覧画面を表示させる処理
                        Button(action: {
                            status_post = true
                        }){
                            Text("自分の投稿").foregroundColor(Color.black)
                        }.sheet(isPresented: $status_post, onDismiss: {
                            status_post = false
                        }){
                            PostView(name: $nameAppStorage)
                        }
                        //ブロックしたユーザー画面を表示させる処理
                        Button(action: {
                            status_block = true
                        }){
                            Text("ブロックしたユーザー").foregroundColor(Color.black)
                        }.sheet(isPresented: $status_block, onDismiss: {
                            status_block = false
                        }){
                            BlockView()
                        }
                        //ログインせず、ゲスト状態であればログインボタンを表示
                        if nameAppStorage == "ゲスト" {
                            Button(action: {
                                testAppStorage.toggle()
                            }){
                                Text("ログインする").foregroundColor(Color.blue)
                            }
                        } else {
                            //名前の変更画面を表示させる処理
                            Button(action: {
                                status_name = true
                            }){
                                Text("名前変更").foregroundColor(Color.black)
                            }.sheet(isPresented: $status_name, onDismiss: {
                                status_name = false
                            }){
                                NameView(db_Model : db_Model)
                            }
                            //アカウント削除ボタンでアラートを出す処理
                            Button(action: {
                                showingAlert = true
                            }){
                                Text("アカウントを削除").foregroundColor(Color.red)
                            }.alert(isPresented: $showingAlert) {
                                Alert(title: Text("このアカウントを削除しますか?"),
                                      message: Text("1度削除したら再登録が必要になります。"),
                                      primaryButton: .cancel(Text("キャンセル")),    // キャンセル用
                                      //Firebase、CoreDataからアカウント情報を削除
                                      secondaryButton: .destructive(Text("削除"), action: {
                                    let delref = Database.database().reference(withPath: "Person/\(nameAppStorage)")
                                    delref.removeValue()
                                    deletePerson()
                                }))
                            }
                        }
                    }.listRowBackground(Color.white)
                }
            }.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)
                }
            })
            .task {
                //自分の投稿情報をCloudKitより更新する度に取得
                await viewModel.fetchItem_mypost(name: nameAppStorage)
                //いいね数をCloudKitより更新する度に取得
                Good_sum(threadins: viewModel.items)
                await viewModel.checkAccountStatus()
            }
            //CloudKitよりデータの取得に失敗した際のエラーハンドリング
            .redacted(reason: viewModel.isLoading ? .placeholder : [])
            //ユーザがスワイプまたはリフレッシュアクションを実行したときにデータを更新する
            .refreshable {
                await viewModel.fetchItem_mypost(name: nameAppStorage)
            }
        }.navigationViewStyle(.stack)
    }
    //CloudKitから自分の投稿の合計いいね数を取得し、変数に格納
    func Good_sum(threadins: [ThreadinItem] ) {
        good_sum = 0
        for threadin in threadins {
            good_sum += Int(threadin.good)
        }
    }
    
    //CoreData内のアカウント情報を削除
    func deletePerson() {
        for person in persons{
            viewContext.delete(person)
        }
        do {
            try viewContext.save()
        } catch {
            fatalError("セーブに失敗")
        }
        testAppStorage.toggle()
    }
}

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
}()

以下がいいねした投稿を表示する画面の説明になります。

struct ManageView: View {

    @State var id = ""
    @State var threadname = ""
    @State var title = ""
    @State var report = ""
    @State var post_name = ""
    @State var date = Date()
    //trueで投稿にいいねした状態
    @State var tf = false
    @State var selectedImaged:Data = Data.init()
    //trueで投稿の内容を表示
    @State var thread_state = false

    @Environment(\.presentationMode) var presentationMode

    //自分がいいねをした投稿の情報をCoreDataより取得
    @FetchRequest(
     sortDescriptors: [NSSortDescriptor(keyPath: \Manage.threadname, ascending: false)],
     animation: .default)
     private var manages: FetchedResults<Manage>
    
    @ObservedObject var db_Model : DB_Model
     
    var body: some View {
        VStack{
            List{
                ForEach(manages, id: \.self){manage in
                    //CoreData内にあるいいねをした投稿のみを取得
                    if manage.good_state == true {
                        //投稿の中身を表示する処理
                        Button(action: {
                            id = manage.thread_id!
                            title = manage.title!
                            report = manage.report!
                            post_name = manage.post_name!
                            date = manage.timestamp!
                            threadname = manage.threadname!
                            if manage.imagedata != nil {
                                selectedImaged = manage.imagedata!
                            }
                            //投稿内容を表示する画面でいいねした状態で表示
                            tf = true
                            self.thread_state.toggle()
                        }){
                            VStack(alignment: .leading, spacing: 10) {
                                HStack{
                                    Image(systemName: "person.fill")
                                    Text(manage.post_name!)
                                    Spacer()
                                    Text("\(manage.timestamp!, formatter: itemFormatter)  ")
                                }
                                HStack {
                                    Text(manage.title!).fontWeight(.medium).font(.system(size: 20, design: .default))
                                    Spacer()
                                }
                                HStack{
                                    Spacer()
                                    Text("スレッド名:\(manage.threadname!)")
                                }
                            }.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)
            }
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }){
                Text("閉じる").padding()
            }
        }
    }
}

以下が自分の投稿を表示する画面の説明になります。

struct PostView: View {

    @State var title = ""
    @State var threadname = ""

    //前画面より自分の名前を引き継ぎ
    @Binding var name:String

    @Environment(\.managedObjectContext) private var viewContext
    @Environment(\.presentationMode) var presentationMode

    //CoreDataより自分の投稿情報を取得
    @FetchRequest(
     sortDescriptors: [NSSortDescriptor(keyPath: \Myposts.threadname, ascending: false)],
     animation: .default)
     private var myposts: FetchedResults<Myposts>
    
    @StateObject var viewModel = ThreadinListViewModel()
    
    var body: some View {
        VStack{
            List{
                HStack{
                    Spacer()
                    Text("※投稿を削除したい場合は左へスワイプ※").font(.callout).bold().frame(alignment: .center)
                    Spacer()
                }.listRowBackground(Color.orange)
                
                ForEach(myposts, id: \.self){mypost in
                    VStack(alignment: .leading, spacing: 10) {
                        HStack{
                            Text("\(mypost.timestamp!, formatter: itemFormatter)")
                            Spacer()
                        }
                        HStack {
                            Text(mypost.title!).fontWeight(.medium).font(.system(size: 20, design: .default))
                            Spacer()
                        }
                        HStack {
                         Spacer()
                            Text("スレッド名:" + mypost.threadname!)
                         }
                    //投稿対してスワイプできるようにする処理(右から左へスワイプ)
                    }.swipeActions(edge: .trailing, allowsFullSwipe: false) {
                        //削除(ゴミ箱)ボタンを押された時の処理。role: .destructiveでデータの削除やアクションの取り消しに関連するボタンであることを明示
                        Button(role: .destructive) {
                            //CloudKitからデータを削除
                            Task.detached {
                                await viewModel.deleteItem(with: mypost.thread_id!)
                            }
                            //CoreDataからデータを削除
                            viewContext.delete(mypost)
                            //Firebaseのデータを削除
                            let ref = Database.database().reference(withPath: "threadin/\(mypost.threadname!)/\(mypost.thread_id!)")
                            ref.removeValue()
                        } label: {
                            Image(systemName: "trash.fill")
                        }
                    }.foregroundColor(Color.black)
                }.listRowBackground(Color.white)
            }
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }){
                Text("閉じる").padding()
            }
        }
    }
}

以下がブロックしたユーザーを表示する画面の説明になります。

struct BlockView: View {

    @Environment(\.managedObjectContext) private var viewContext
    @Environment(\.presentationMode) var presentationMode

    //CoreDataからブロックしたユーザー情報を取得
    @FetchRequest(sortDescriptors: []) var blocks: FetchedResults<Block>
    
    var body: some View {
        VStack{
            List{
                ForEach(blocks, id: \.self){block in
                    VStack(alignment: .leading, spacing: 10) {
                        HStack{
                            Text(block.post_name!)
                            Spacer()
                            //ブロックを解除する処理。CoreData内のブロックユーザー情報を削除
                            Button(action: {
                                viewContext.delete(block)
                                do {
                                    try viewContext.save()
                                } catch {
                                    fatalError("セーブに失敗")
                                    
                                }
                            }){
                                Text("ブロック解除").foregroundColor(.blue)
                            }
                        }
                    }.foregroundColor(Color.black)
                }.listRowBackground(Color.white)
            }
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }){
                Text("閉じる").padding()
            }
        }
    }
}

以下が名前を変更する画面の説明になります。

struct NameView: View {
    //新しい名前を格納する変数
    @State var name = ""
    //登録済みの名前であればエラー文字を格納する変数
    @State var yes_name = ""

    @AppStorage("myname") var nameAppStorage = ""

    @Environment(\.presentationMode) var presentationMode
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(sortDescriptors: []) var persons: FetchedResults<Person>

    @ObservedObject var db_Model : DB_Model
    
    let ref = Database.database().reference()
    
    var body: some View {
        VStack{
            TextField("名前", text:$name).textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
                Text(yes_name).font(.system(size: 13)).foregroundColor(.red)
                HStack {
                    Button(action: {
                        self.presentationMode.wrappedValue.dismiss()
                    }){
                        Text("キャンセル").padding()
                    }
                    //名前の変更を保存する処理
                    Button(action: {
                        yes_name = ""
                        //Firebaseより登録されている名前の一覧を取得
                        ref.child("Person").observeSingleEvent(of: .value, with: { snapshot in
                            if let value = snapshot.value as? [String:String] {
                                //名前が登録済みか否かを判断
                                for i in value {
                                    if name == i.value || name == ""{
                                        yes_name = "そのユーザー名は既に使用されています"
                                    }
                                }
                                //名前が登録済みでなければ実行する処理
                                if yes_name == "" {
                                    //Firebaseへ新規追加
                                    ref.child("Person/" + name).setValue(name)
                                    //Firebaseにある以前の名前情報を削除
                                    let delref = Database.database().reference(withPath: "Person/\(nameAppStorage)")
                                    delref.removeValue()
                                    nameAppStorage = name
                                    //CoreDataにあるアカウント情報を削除
                                    db_Model.editPerson(item: persons[0])
                                    db_Model.writePerson(context: viewContext)
                                    self.presentationMode.wrappedValue.dismiss()
                                }
                            }
                        })
                    }){
                        Text("変更").padding()
                    }
            }
        }
    }
}

以上がマイページ画面の説明になります。このアプリ内で使っているCloudKitに格納するための処理や画像を写真アプリから選択する処理などを細かく説明していきます。アプリ開発は初めての方にとっては難しいと思います。こ私のこの記事や他の開発に関する記事が見てくださる皆様の力になれますと幸いです。

アプリ”NowShare”をダウンロード
情報共有アプリ"NowShare"
今からみんなでナレッジ共有! 本当に欲しい情報をキャッチ&リリース 教えてあげたい情報を知りたい人にすぐに共有。 ためになった情報にはいいねを押して盛り上げよう!
Appエンジニア
ナスジニアをフォローする
🍆ナスジニアのブログ(iPhoneアプリ開発)

コメント

タイトルとURLをコピーしました