Zenn
Open14

毎日Swift

スポーツザリガニスポーツザリガニ

3/14
昨日は荷解きで体力0になりそのまま力尽きてしまった。。

・進捗

struct ContentView: View {
    var body: some View {
        ScrollView {
            VStack {
                HStack{
                    Text("Today")
                        .font(.title)
                        .fontWeight(.bold)
                        .padding()
                    Text("3月14日")
                        .foregroundColor(.gray)
                    Spacer()
                    Image(systemName:"person.crop.circle.fill")
                        .imageScale(.large)
                        .foregroundColor(.gray)
                        .padding()
                }
                
                ZStack{
                    Image("neko1")
                        .scaledToFit()
                        .frame(width: 350, height: 400)
                        .cornerRadius(30)
                    Text("基本を知る")
                        .font(.title)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                    Spacer()
                    
                    Text("いざという時に行動できる情報を届ける")
                        .font(.title)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                        .padding(.top,100)
                    
                    VStack{
                        Spacer()
                        ZStack{
                            RoundedRectangle(cornerRadius: 20)
                                .fill(Color.white.opacity(0.7))
                                .frame(width: 350, height: 100)
                            
                            HStack{
                                Image(systemName: "text.rectangle.page")
                                Text("Hello, world!")
                                    .font(.title)
                                    .fontWeight(.bold)
                                    .foregroundColor(.white)
                                Capsule()
                                    .scaledToFit()
                                    .frame(width:40)
                            }
                        }
                    }
                }
                Image("neko1")
                    .scaledToFit()
                    .frame(width: 350, height: 150)
                    .cornerRadius(30)
                    .padding()
            }
            Spacer()
            HStack{
                Image(systemName: "text.rectangle.page")
                Image(systemName: "rugbyball")
                Image(systemName: "square.3.layers.3d")
                Image(systemName: "arcade.stick")
                Image(systemName: "magnifyingglass")
            }
        }
    }
}

#Preview {
    ContentView()
}

◾️今日やったこと
・画面一番上の「Today」と日付(とりあえず直書き)とアイコンを追加。
・neko1の画像に重ねてるRoundedRectangleとその中のtextをSpacer()で押し下げて画像の下端に上手く配置できた。中身もHStackでより似せることができた。
・画面下部のボタン群をとりあえずSFシンボルズの画像だけ並べた。

◾️できなかったこと
・Text("基本を知る")とText("いざという時に行動できる情報を届ける")の配置。
左寄せにしたいんだけどなんか上手くいかないし見切れる。。
RoundedRectangleの中の横並びの要素も拡げたい。

・画面下部のボタン群はスクロールされずに画面下部に表示されてほしいけど、一緒に動いてしまう。
Scrollviewの外に出せばいいんだと思うのだけどエラーになる。あとアイコンの下に小さくテキストも追加したいし間隔も拡げたいのだけど、いちいち1個1個VStackで囲ったりするのか、、?ループ処理みたいなんでどうにかなるか?

★明日やること
・とりあえず画面下部のボタン群は空でいいからボタンにする。

目指してるレイアウト↓

スポーツザリガニスポーツザリガニ

3/15
今日は結構ChatGPTにお世話になってしまったけどかなり進んだ。

◾️進んだこと
・日付をdate型に変更
・ボタン群を画像を並べただけのものをボタンにして、配列でまとめた。
スクロールビューの外に出して、.ignoressafeAreaで画面の下端に固定できた。

・学び
.fontにfontweightはまとめられる。「 .font(.title.bold())」
・日付を「May.15」から「3月15日」に変更するのは「.locale(Locale(identifier: "ja_JP"))」
Paddingのデフォルトは16に設定されてる。それよりも小さくするのは10とかにすればいい。
テキストは図形に重ねなくても、バックグラウンドカラーをつければいい。

★明日やること
Textの位置を左端に寄せる

//
//  ContentView.swift
//  MarchHomework
//
//  Created by tomohide katagiri on 2025/03/13.
//

import SwiftUI

struct BottomButton: Identifiable {
    let id = UUID()
    let title: String
    let systemImageName: String
}

let bottomButtons: [BottomButton] = [
    BottomButton(title: "Today", systemImageName: "book.fill"),
    BottomButton(title: "ゲーム", systemImageName: "rugbyball"),
    BottomButton(title: "アプリ", systemImageName: "square.stack.fill"),
    BottomButton(title: "Arcade", systemImageName: "arcade.stick"),
    BottomButton(title: "検索", systemImageName: "magnifyingglass")
]

struct ContentView: View {
    var body: some View {
        ZStack {
            ScrollView {
                VStack {
                    HStack{
                        Text("Today")
                            .font(.title.bold())
                            .padding()
                        Text(Date().formatted(.dateTime.month().day().locale(Locale(identifier: "ja_JP"))))
                            .foregroundColor(.gray)
                        Spacer()
                        Image(systemName:"person.crop.circle.fill")
                            .imageScale(.large)
                            .foregroundColor(.gray)
                            .padding()
                    }
                    
                    ZStack{
                        Image("neko1")
                            .scaledToFit()
                            .frame(width: 350, height: 400)
                            .cornerRadius(30)
                        Text("基本を知る")
                            .font(.title.bold())
                            .foregroundColor(.white)
                        Spacer()
                        
                        Text("いざという時に行動できる情報を届ける")
                            .font(.title.bold())
                            .foregroundColor(.white)
                            .padding(.top,100)
                        
                        VStack{
                            Spacer()
                            ZStack{
                                RoundedRectangle(cornerRadius: 20)
                                    .fill(Color.white.opacity(0.7))
                                    .frame(width: 350, height: 100)
                                
                                HStack{
                                    Image(systemName: "text.rectangle.page")
                                    Text("Hello, world!")
                                        .font(.title.bold())
                                        .foregroundColor(.white)
                                    Text("入手")
                                        .padding(.horizontal, 10)
                                        .font(.title.bold())
                                        .padding(.horizontal,8)
                                        .foregroundColor(.white)
                                        .background(Color.gray.opacity(0.6))
                                        .clipShape(RoundedRectangle(cornerRadius: 30))
                                }
                            }
                        }
                    }
                    
                    Image("neko1")
                        .scaledToFit()
                        .frame(width: 350, height: 150)
                        .cornerRadius(30)
                        .padding()
                }
            }
            //こっからボタン
            VStack {
                Spacer()
                HStack(spacing: 40) {
                    ForEach(bottomButtons) { button in
                        Button(action: {
                            print("\(button.title) ボタンを押したよ〜♡")
                        }) {
                            VStack {
                                Image(systemName: button.systemImageName)
                                    .foregroundColor(.gray)
                                Text(button.title)
                                    .font(.caption)
                                    .foregroundColor(.gray)
                            }
                        }
                    }
                }
                .padding(.vertical, 10)
                .frame(maxWidth: .infinity)
                .background(
                    VStack {
                        Rectangle()
                            .frame(height: 0.5)
                            .foregroundColor(Color.gray)
                        Spacer()
                    }
                        .background(Color.white.opacity(0.9)))
                .ignoresSafeArea(edges: .bottom)
            }
        }
        
    }
}

#Preview {
    ContentView()
}

スポーツザリガニスポーツザリガニ

3/16

今日は見た目の微調整だけ。

◾️今日やったこと
・日付の行のサイズと位置の調整
・画像の差し替え
・文字の改行と位置の調整

◾️学んだこと
・改行は\n
・offsetで位置の調整はできるけどレイアウト崩れの原因になるので、基本的にはpaddingで調整する
・.bold()と.weight(.bold)は実はちょっと違う
.bold()はFont型に用意された簡易的な共通メソッド
.weight()は細かく調整が可能だけど、.heavy()みたいに省略して呼び出すことはできない

◾️明日やること
・2段目の画像にテキストやサムネイルの画像を重ねる
・Appstoreみたいに2段目以降も画像を追加する
これもForEachでダーッと増やせたらいいな、、、けどイメージできていない。
・ChatGPTに聞いたらそろそろViewを分けたほうがいいって言われた。
Udemyではこのへんでつまづいたからかなり腰が重い、、、ChatGPTはやり方を教えてくれたけど、Udemy同様コピペするだけになりそうで怖い(画面遷移つくるのはもっともっと怖い)。

//
//  ContentView.swift
//  MarchHomework
//
//  Created by tomohide katagiri on 2025/03/13.
//

import SwiftUI

struct BottomButton: Identifiable {
    let id = UUID()
    let title: String
    let systemImageName: String
}

let bottomButtons: [BottomButton] = [
    BottomButton(title: "Today", systemImageName:"text.rectangle.page"),
    BottomButton(title: "ゲーム", systemImageName: "rugbyball"),
    BottomButton(title: "アプリ", systemImageName: "square.3.layers.3d"),
    BottomButton(title: "Arcade", systemImageName: "arcade.stick"),
    BottomButton(title: "検索", systemImageName: "magnifyingglass")
]

struct ContentView: View {
    var body: some View {
        ZStack {
            ScrollView {
                VStack {
                    //日付とアイコンの部分
                    HStack{
                        Text("Today")
                            .font(.system(size: 40).weight(.heavy))
                            .padding()
                        Text(Date().formatted(.dateTime.month().day().locale(Locale(identifier: "ja_JP"))))
                            .font(.system(size: 25).weight(.bold))
                            .foregroundColor(.gray)
                            .padding(.top, 10)
                        Spacer()
                        
                        Image(systemName:"person.crop.circle.fill")
                            .font(.system(size: 40))
                            .foregroundColor(.gray)
                            .padding()
                            .padding(.top, 10)
                    }
                    //スクロールする部分
                    ZStack{
                        Image("neko1")
                            .scaledToFit()
                            .frame(width: 350, height: 400)
                            .cornerRadius(30)
                        
                        HStack {
                            Text("基本を知る")
                                .font(.body.bold())
                                .foregroundColor(.white)
                                .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                .padding()
                            Spacer()
                        }
                        .frame(width: 350)
                        
                        HStack {
                            Text("いざという時に\n行動できる情報を届ける")
                                .font(.title.bold())
                                .foregroundColor(.white)
                                .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                .padding(.top,100)
                                .padding()
                            Spacer()
                        }
                        .frame(width: 350)
                        
                        VStack{
                            Spacer()
                            ZStack {
                                RoundedRectangle(cornerRadius: 20)
                                    .fill(Color.black.opacity(0.5))
                                    .frame(width: 350, height: 90)
                                
                                HStack(spacing: 0) {
                                    
                                    Image("neko2")
                                        .resizable()
                                        .scaledToFill()
                                        .frame(width: 60, height: 60)
                                        .cornerRadius(10)
                                        .padding(.trailing, 15)
                                    
                                    VStack(alignment: .leading, spacing: 5) {
                                        Text("nekoo.防災訓練")
                                            .font(.headline.bold())
                                            .foregroundColor(.white)
                                            .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                        
                                        Text("地震や災害などの災害情報\nを通知")
                                            .font(.caption.bold())
                                            .foregroundColor(.white)
                                    }
                                    .padding(.trailing,20)
                                    Text("入手")
                                        .padding(8)
                                        .font(.system(size: 20).weight(.bold))
                                        .padding(.horizontal,15)
                                        .foregroundColor(.white)
                                        .background(Color.gray.opacity(0.6))
                                        .clipShape(RoundedRectangle(cornerRadius: 30))
                                }
                            }
                        }
                    }
                    
                    Image("neko3")
                        .resizable()
                        .scaledToFill()
                        .frame(width: 350, height: 150)
                        .offset(y: 80)
                        .clipped()
                        .cornerRadius(30)
                        .padding()
                }
            }
            //ボタン部分
            VStack {
                Spacer()
                HStack(spacing: 40) {
                    ForEach(bottomButtons) { button in
                        Button(action: {
                            print("\(button.title) ボタンを押したよ〜♡")
                        }) {
                            VStack {
                                Image(systemName: button.systemImageName)
                                    .foregroundColor(.gray)
                                    .padding(3)
                                Text(button.title)
                                    .font(.caption)
                                    .foregroundColor(.gray)
                            }
                        }
                    }
                }
                .padding(.vertical, 10)
                .frame(maxWidth: .infinity)
                .background(
                    VStack {
                        Rectangle()
                            .frame(height: 0.5)
                            .foregroundColor(Color.gray)
                        Spacer()
                    }
                        .background(Color.white.opacity(0.9)))
                .ignoresSafeArea(edges: .bottom)
            }
        }
        
    }
}

#Preview {
    ContentView()
}

スポーツザリガニスポーツザリガニ

3/17
◾️やったこと
スクロール部分に画像とテキストを追加しただけ。

◾️明日以降やること
関数にまとめて呼び出すのをやりたいけど苦手意識が先行してしまって避け続けた結果、繰り返しの部分もそのまんま書いており、
アホみたいに長くなってしまっている。明日以降スッキリさせる。やる。
エフェクターもプログラマブルスイッチャーとかMIDI同期から逃げて、「直列で好きな歪み3つとリバーブ繋ぐのが一番かっこいい」という宗教に入信した過去があり、その時の思考から抜け出せていない。

//
//  ContentView.swift
//  MarchHomework
//
//  Created by tomohide katagiri on 2025/03/13.
//

import SwiftUI

struct BottomButton: Identifiable {
    let id = UUID()
    let title: String
    let systemImageName: String
}

let bottomButtons: [BottomButton] = [
    BottomButton(title: "Today", systemImageName:"text.rectangle.page"),
    BottomButton(title: "ゲーム", systemImageName: "rugbyball"),
    BottomButton(title: "アプリ", systemImageName: "square.3.layers.3d"),
    BottomButton(title: "Arcade", systemImageName: "arcade.stick"),
    BottomButton(title: "検索", systemImageName: "magnifyingglass")
]

struct ContentView: View {
    var body: some View {
        ZStack {
            ScrollView {
                VStack {
                    //日付とアイコンの部分
                    HStack{
                        Text("Today")
                            .font(.system(size: 40).weight(.heavy))
                            .padding()
                        Text(Date().formatted(.dateTime.month().day().locale(Locale(identifier: "ja_JP"))))
                            .font(.system(size: 25).weight(.bold))
                            .foregroundColor(.gray)
                            .padding(.top, 10)
                        Spacer()
                        
                        Image(systemName:"person.crop.circle.fill")
                            .font(.system(size: 40))
                            .foregroundColor(.gray)
                            .padding()
                            .padding(.top, 10)
                    }
                    //スクロールする部分
                    ZStack{
                        Image("neko1")
                            .scaledToFit()
                            .frame(width: 350, height: 400)
                            .cornerRadius(30)
                        
                        HStack {
                            Text("基本を知る")
                                .font(.body.bold())
                                .foregroundColor(.white)
                                .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                .padding()
                            Spacer()
                        }
                        .frame(width: 350)
                        
                        HStack {
                            Text("いざという時に\n行動できる情報を届ける")
                                .font(.title.bold())
                                .foregroundColor(.white)
                                .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                .padding(.top,100)
                                .padding()
                            Spacer()
                        }
                        .frame(width: 350)
                        
                        VStack{
                            Spacer()
                            ZStack {
                                VStack{
                                    RoundedRectangle(cornerRadius: 20)
                                        .fill(Color.black.opacity(0.5))
                                        .frame(width: 350, height: 90)
                                }
                                
                                HStack(spacing: 0) {
                                    Image("neko2")
                                        .resizable()
                                        .scaledToFill()
                                        .frame(width: 60, height: 60)
                                        .cornerRadius(10)
                                        .padding(.trailing, 15)
                                    
                                    VStack(alignment: .leading, spacing: 5) {
                                        Text("nekoo.防災訓練")
                                            .font(.headline.bold())
                                            .foregroundColor(.white)
                                            .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                        
                                        Text("地震や災害などの災害情報\nを通知")
                                            .font(.caption.bold())
                                            .foregroundColor(.white)
                                    }
                                    .padding(.trailing,20)
                                    Text("入手")
                                        .padding(8)
                                        .font(.system(size: 20).weight(.bold))
                                        .padding(.horizontal,15)
                                        .foregroundColor(.white)
                                        .background(Color.gray.opacity(0.6))
                                        .clipShape(RoundedRectangle(cornerRadius: 30))
                                }
                            }
                        }
                    }
                    
                    ZStack{
                        Image("neko3")
                            .resizable()
                            .scaledToFill()
                            .frame(width: 350, height: 150)
                            .offset(y: 80)
                            .clipped()
                            .cornerRadius(30)
                            .padding()
                        
                        HStack{
                            VStack{
                                HStack{
                                    Text("SDニャンダム")
                                        .font(.headline.bold())
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                        .padding(.trailing,130)
                                        .padding(.top,80)
                                }
                                
                                HStack{
                                    Text("広告")
                                        .padding(4)
                                    
                                        .font(.callout)
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.8), radius: 3, x: 0, y: 2)
                                        .background(Color.blue.opacity(0.6))
                                        .clipShape(RoundedRectangle(cornerRadius: 10))
                                        .padding(.top,-15)
                                        .padding(.trailing,-20)
                                    Text("近日公開")
                                        .padding(8)
                                        .padding(.horizontal,5)
                                        .font(.callout.bold())
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.8), radius: 3, x: 0, y: 2)
                                        .padding(.top,-15)
                                        .padding(.trailing,120)
                                }
                                
                            }
                            Text("入手")
                                .padding(8)
                                .font(.system(size: 20).weight(.bold))
                                .padding(.horizontal,15)
                                .foregroundColor(.white)
                                .background(Color.gray.opacity(0.6))
                                .clipShape(RoundedRectangle(cornerRadius: 30))
                                .padding(.top,60)
                        }
                    }
                    VStack(spacing: 0) {
                        ZStack {
                            VStack{
                                RoundedRectangle(cornerRadius: 20)
                                    .fill(Color.black.opacity(0.5))
                                    .frame(width: 350, height: 400)
                            }
                            
                            VStack{
                                HStack {
                                    Text("トレンド")
                                        .font(.body.bold())
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                        .padding()
                                    Spacer()
                                }
                                .frame(width: 350)
                                
                                HStack {
                                    Text("注目のドラマ・映画・\n番組・スポーツ観戦")
                                        .font(.title.bold())
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                        
                                }
                                
                                HStack(spacing: 0) {
                                    Image("neko2")
                                        .resizable()
                                        .scaledToFill()
                                        .frame(width: 60, height: 60)
                                        .cornerRadius(10)
                                        .padding(.trailing, 15)
                                    
                                    VStack(alignment: .leading, spacing: 5) {
                                        Text("MeowTube")
                                            .font(.headline.bold())
                                            .foregroundColor(.white)
                                            .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                        
                                        Text("公式アプリで動画と音楽")
                                            .font(.caption.bold())
                                            .foregroundColor(.white)
                                    }
                                    .padding(.trailing,20)
                                    Text("入手")
                                        .padding(8)
                                        .font(.system(size: 20).weight(.bold))
                                        .padding(.horizontal,15)
                                        .foregroundColor(.white)
                                        .background(Color.gray.opacity(0.6))
                                        .clipShape(RoundedRectangle(cornerRadius: 30))
                                }
                                HStack(spacing: 0) {
                                    Image("neko2")
                                        .resizable()
                                        .scaledToFill()
                                        .frame(width: 60, height: 60)
                                        .cornerRadius(10)
                                        .padding(.trailing, 15)
                                    
                                    VStack(alignment: .leading, spacing: 5) {
                                        Text("TikCats")
                                            .font(.headline.bold())
                                            .foregroundColor(.white)
                                            .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                        
                                        Text("もっと猫を好きになる。")
                                            .font(.caption.bold())
                                            .foregroundColor(.white)
                                    }
                                    .padding(.trailing,20)
                                    Text("入手")
                                        .padding(8)
                                        .font(.system(size: 20).weight(.bold))
                                        .padding(.horizontal,15)
                                        .foregroundColor(.white)
                                        .background(Color.gray.opacity(0.6))
                                        .clipShape(RoundedRectangle(cornerRadius: 30))
                                }
                                HStack(spacing: 0) {
                                    Image("neko2")
                                        .resizable()
                                        .scaledToFill()
                                        .frame(width: 60, height: 60)
                                        .cornerRadius(10)
                                        .padding(.trailing, 15)
                                    
                                    VStack(alignment: .leading, spacing: 5) {
                                        Text("Ben cat Meaw")
                                            .font(.headline.bold())
                                            .foregroundColor(.white)
                                            .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                        
                                        Text("Originals,movies,cats")
                                            .font(.caption.bold())
                                            .foregroundColor(.white)
                                    }
                                    .padding(.trailing,20)
                                    Text("入手")
                                        .padding(8)
                                        .font(.system(size: 20).weight(.bold))
                                        .padding(.horizontal,15)
                                        .foregroundColor(.white)
                                        .background(Color.gray.opacity(0.6))
                                        .clipShape(RoundedRectangle(cornerRadius: 30))
                                }
                            }
                        }
                        .padding(.bottom, 200)
                    }
                }
            }
            //ボタン部分
            VStack {
                Spacer()
                HStack(spacing: 40) {
                    ForEach(bottomButtons) { button in
                        Button(action: {
                            print("\(button.title) ボタンを押したよ〜♡")
                        }) {
                            VStack {
                                Image(systemName: button.systemImageName)
                                    .foregroundColor(.gray)
                                    .padding(3)
                                Text(button.title)
                                    .font(.caption)
                                    .foregroundColor(.gray)
                            }
                        }
                    }
                }
                .padding(.vertical, 10)
                .frame(maxWidth: .infinity)
                .background(
                    VStack {
                        Rectangle()
                            .frame(height: 0.5)
                            .foregroundColor(Color.gray)
                        Spacer()
                    }
                        .background(Color.white.opacity(0.9)))
                .ignoresSafeArea(edges: .bottom)
            }
        }
    }
}

#Preview {
    ContentView()
}

スポーツザリガニスポーツザリガニ

出社+眼精疲労で「帰宅して夕食後にちょっと目を休ませてからやろう」と思ったら二日連続でそのまま寝落ちしてしまった。。。

◾️今日やったこと
コンポーネントを別のviewにわけた!

メイン

import SwiftUI

//AppData の定義
struct AppData: Identifiable {
    let id = UUID()
    let imageName: String
    let title: String
    let description: String
    let textColor: Color
    let backgroundColor: Color
}

//BottomButton の定義
struct BottomButton: Identifiable {
    let id = UUID()
    let title: String
    let systemImageName: String
}

//アプリデータ
let apps: [AppData] = [
    AppData(imageName: "neko2", title: "MeowTube", description: "公式アプリで動画と音楽", textColor: .black, backgroundColor: .white),
    AppData(imageName: "neko2", title: "TikCats", description: "もっと猫を好きになる。", textColor: .black, backgroundColor: .white),
    AppData(imageName: "neko2", title: "Ben cat Meaw", description: "Originals,movies,cats", textColor: .black, backgroundColor: .white)
]

// ボトムバーのボタンデータ
let bottomButtons: [BottomButton] = [
    BottomButton(title: "Today", systemImageName:"text.rectangle.page"),
    BottomButton(title: "ゲーム", systemImageName: "rugbyball"),
    BottomButton(title: "アプリ", systemImageName: "square.3.layers.3d"),
    BottomButton(title: "Arcade", systemImageName: "arcade.stick"),
    BottomButton(title: "検索", systemImageName: "magnifyingglass")
]


struct ContentView: View {
    var body: some View {
        ZStack {
            Color.gray.opacity(0.2)
                .edgesIgnoringSafeArea(.all)
            
            ScrollView {
                VStack {
                    // ヘッダー部分(日付&アイコン)
                    HStack {
                        Text("Today")
                            .font(.system(size: 40).weight(.heavy))
                            .padding()
                        Text(Date().formatted(.dateTime.month().day().locale(Locale(identifier: "ja_JP"))))
                            .font(.system(size: 25).weight(.bold))
                            .foregroundColor(.gray)
                            .padding(.top, 10)
                        Spacer()
                        Image(systemName:"person.crop.circle.fill")
                            .font(.system(size: 40))
                            .foregroundColor(.gray)
                            .padding()
                            .padding(.top, 10)
                    }
                    
                    // メインコンテンツ(スクロール部分)
                    ZStack {
                        Image("neko1")
                            .scaledToFit()
                            .frame(width: 350, height: 400)
                            .cornerRadius(30)
                        
                        HStack {
                            Text("基本を知る")
                                .font(.body.bold())
                                .foregroundColor(.white)
                                .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                .padding()
                            Spacer()
                        }
                        .frame(width: 350)
                        
                        HStack {
                            Text("いざという時に\n行動できる情報を届ける")
                                .font(.title.bold())
                                .foregroundColor(.white)
                                .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                .padding(.top, 100)
                                .padding()
                            Spacer()
                        }
                        .frame(width: 350)
                        
                        VStack {
                            Spacer()
                            ZStack {
                                AppCardView(imageName: "neko2", title: "nekoo.防災訓練", description: "地震や災害などの災害情報\nを通知", textColor: .white, backgroundColor: .gray.opacity(0.5))
                            }
                        }
                    }
                    // 広告ブロック
                    ZStack {
                        Image("neko3")
                            .resizable()
                            .scaledToFill()
                            .frame(width: 350, height: 150)
                            .offset(y: 80)
                            .clipped()
                            .cornerRadius(30)
                            .padding()
                        
                        HStack {
                            VStack {
                                Text("SDニャンダム")
                                    .font(.headline.bold())
                                    .foregroundColor(.white)
                                    .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                    .padding(.trailing, 130)
                                    .padding(.top, 80)
                                HStack {
                                    Text("広告")
                                        .padding(4)
                                        .font(.callout)
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.8), radius: 3, x: 0, y: 2)
                                        .background(Color.blue.opacity(0.6))
                                        .clipShape(RoundedRectangle(cornerRadius: 10))
                                        .padding(.top, -15)
                                        .padding(.trailing, -20)
                                    Text("近日公開")
                                        .padding(8)
                                        .padding(.horizontal, 5)
                                        .font(.callout.bold())
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.8), radius: 3, x: 0, y: 2)
                                        .padding(.top, -15)
                                        .padding(.trailing, 120)
                                }
                            }
                            PurchaseButtonView {
                                print("広告ブロックの入手ボタンが押された!")
                            }
                            .padding(.top, 60)
                        }
                    }
                    
                    // トレンドアプリのリスト
                    VStack(spacing: 0) {
                        ZStack {
                            RoundedRectangle(cornerRadius: 20)
                                .fill(Color.white.opacity(0.5))
                                .frame(width: 350, height: 400)
                            
                            VStack {
                                Text("トレンド")
                                    .font(.body.bold())
                                    .foregroundColor(.gray)
                                    .padding()
                                    .frame(width: 350, alignment: .leading)
                                Text("注目のドラマ・映画・\n番組・スポーツ観戦")
                                    .font(.title.bold())
                                    .foregroundColor(.black)
                                
                                ForEach(apps) { app in
                                    AppCardView(
                                        imageName: app.imageName,
                                        title: app.title,
                                        description: app.description,
                                        textColor: app.textColor,
                                        backgroundColor: app.backgroundColor
                                    )
                                }
                            }
                        }
                    }
                    .padding(.bottom, 200)
                }
            }
            
            // ボトムナビゲーションバー
            VStack {
                Spacer()
                BottomBarView()
            }
        }
    }
}

#Preview {
    ContentView()
}



アプリのカード

struct AppCardView: View {
    let imageName: String
    let title: String
    let description: String
    let textColor: Color
    let backgroundColor: Color
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(backgroundColor)
                .frame(width: 350, height: 90)
            
            HStack(spacing: 0) {
                Image(imageName)
                    .resizable()
                    .scaledToFill()
                    .frame(width: 60, height: 60)
                    .cornerRadius(10)
                    .padding(.trailing, 15)
                
                VStack(alignment: .leading, spacing: 5) {
                    Text(title)
                        .font(.headline.bold())
                        .foregroundColor(textColor)
                    
                    Text(description)
                        .font(.caption.bold())
                        .foregroundColor(textColor)
                }
                .padding(.trailing, 20)

                PurchaseButtonView {
                    print("\(title) の入手ボタンが押された!")
                }
            }
        }
    }
}

#Preview {
    VStack {
        AppCardView(imageName: "neko2", title: "MeowTube", description: "公式アプリで動画と音楽", textColor: .white, backgroundColor: .blue)
        AppCardView(imageName: "neko2", title: "TikCats", description: "もっと猫を好きになる。", textColor: .black, backgroundColor: .yellow)
    }
}

下のボタンたち

import SwiftUI

struct BottomBarView: View {
    var body: some View {
        HStack(spacing: 40) {
            ForEach(bottomButtons) { button in
                Button(action: {
                    print("\(button.title) ボタンを押したよ〜♡")
                }) {
                    VStack {
                        Image(systemName: button.systemImageName)
                            .foregroundColor(.gray)
                            .padding(3)
                        Text(button.title)
                            .font(.caption)
                            .foregroundColor(.gray)
                    }
                }
            }
        }
        .padding(.vertical, 10)
        .frame(maxWidth: .infinity)
        .background(
            VStack {
                Rectangle()
                    .frame(height: 0.5)
                    .foregroundColor(Color.gray)
                Spacer()
            }
                .background(Color.white.opacity(0.9))
        )
        .ignoresSafeArea(edges: .bottom)
    }
}

#Preview {
    BottomBarView()
}

入手ボタン

import SwiftUI

struct PurchaseButtonView: View {
    var action: () -> Void = {}
    
    var body: some View {
        Button(action: action) {
            Text("入手")
                .padding(8)
                .font(.system(size: 20).weight(.bold))
                .padding(.horizontal, 15)
                .foregroundColor(.white) // 文字色は白で固定
                .background(Color.gray.opacity(0.6)) // 背景色も固定
                .clipShape(RoundedRectangle(cornerRadius: 30))
        }
    }
}

#Preview {
    PurchaseButtonView()
}

スポーツザリガニスポーツザリガニ

3/23
0時過ぎにコーヒー飲むとこうなるのか

◾️やったこと
・フルスクリーンのモーダル遷移だけど下スワイプで閉じれるようにした。
最初は.sheetを試したけど、画面上部の隙間が気になったので.fullscreencoverにした

・なんかよくわかんないView()をChatGPTに言われるがままに追加
・「入手」ボタンの押し心地(押せる範囲とエフェクトの調整)
結局シュミレータではあんまり気持ちよくならなかった。実機で試したらまた違う可能性があるとのこと。

メインの画面

import SwiftUI

//AppData の定義
struct AppData: Identifiable {
    let id = UUID()
    let imageName: String
    let title: String
    let description: String
    let textColor: Color
    let backgroundColor: Color
}

//BottomButton の定義
struct BottomButton: Identifiable {
    let id = UUID()
    let title: String
    let systemImageName: String
}


//アプリデータ
let apps: [AppData] = [
    AppData(imageName: "neko2", title: "MeowTube", description: "公式アプリで動画と音楽", textColor: .black, backgroundColor: .white),
    AppData(imageName: "neko2", title: "TikCats", description: "もっと猫を好きになる。", textColor: .black, backgroundColor: .white),
    AppData(imageName: "neko2", title: "Ben cat Meaw", description: "Originals,movies,cats", textColor: .black, backgroundColor: .white)
]

// ボトムバーのボタンデータ
let bottomButtons: [BottomButton] = [
    BottomButton(title: "Today", systemImageName:"text.rectangle.page"),
    BottomButton(title: "ゲーム", systemImageName: "rugbyball"),
    BottomButton(title: "アプリ", systemImageName: "square.3.layers.3d"),
    BottomButton(title: "Arcade", systemImageName: "arcade.stick"),
    BottomButton(title: "検索", systemImageName: "magnifyingglass")
]


struct ContentView: View {
    @State private var isSheetPresented = false
    var body: some View {
        ZStack {
            Color.gray.opacity(0.2)
                .edgesIgnoringSafeArea(.all)


            
            ScrollView {
                VStack {
                    // ヘッダー部分(日付&アイコン)
                    HStack {
                        Text("Today")
                            .font(.system(size: 40).weight(.heavy))
                            .padding()
                        Text(Date().formatted(.dateTime.month().day().locale(Locale(identifier: "ja_JP"))))
                            .font(.system(size: 25).weight(.bold))
                            .foregroundColor(.gray)
                            .padding(.top, 10)
                        Spacer()
                        Image(systemName:"person.crop.circle.fill")
                            .font(.system(size: 40))
                            .foregroundColor(.gray)
                            .padding()
                            .padding(.top, 10)
                    }
                    
                    // メインコンテンツ(スクロール部分)
                    ZStack {
                        Button(action: {isSheetPresented = true}) {
                            Image("neko1")
                                .scaledToFit()
                                .frame(width: 350, height: 400)
                                .cornerRadius(30)
                        }
                        .fullScreenCover(isPresented: $isSheetPresented){ ModalView(isPresented: $isSheetPresented)
                                .presentationDetents([.large])
                                
                        }
                        
                        HStack {
                            Text("基本を知る")
                                .font(.body.bold())
                                .foregroundColor(.white)
                                .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                .padding()
                            Spacer()
                        }
                        .frame(width: 350)
                        
                        HStack {
                            Text("いざという時に\n行動できる情報を届ける")
                                .font(.title.bold())
                                .foregroundColor(.white)
                                .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                .padding(.top, 100)
                                .padding()
                            Spacer()
                        }
                        .frame(width: 350)
                        
                        VStack {
                            Spacer()
                            ZStack {
                                AppCardView(imageName: "neko2", title: "nekoo.防災訓練", description: "地震や災害などの災害情報\nを通知", textColor: .white, backgroundColor: .gray.opacity(0.5))
                            }
                        }
                    }
                    // 広告ブロック
                    ZStack {
                        Image("neko3")
                            .resizable()
                            .scaledToFill()
                            .frame(width: 350, height: 150)
                            .offset(y: 80)
                            .clipped()
                            .cornerRadius(30)
                            .padding()
                        
                        HStack {
                            VStack {
                                Text("SDニャンダム")
                                    .font(.headline.bold())
                                    .foregroundColor(.white)
                                    .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                    .padding(.trailing, 130)
                                    .padding(.top, 80)
                                HStack {
                                    Text("広告")
                                        .padding(4)
                                        .font(.callout)
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.8), radius: 3, x: 0, y: 2)
                                        .background(Color.blue.opacity(0.6))
                                        .clipShape(RoundedRectangle(cornerRadius: 10))
                                        .padding(.top, -15)
                                        .padding(.trailing, -20)
                                    Text("近日公開")
                                        .padding(8)
                                        .padding(.horizontal, 5)
                                        .font(.callout.bold())
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.8), radius: 3, x: 0, y: 2)
                                        .padding(.top, -15)
                                        .padding(.trailing, 120)
                                }
                            }
                            PurchaseButtonView {
                                print("広告ブロックの入手ボタンが押された!")
                            }
                            .padding(.top, 60)
                        }
                    }
                    
                    // トレンドアプリのリスト
                    VStack(spacing: 0) {
                        ZStack {
                            RoundedRectangle(cornerRadius: 20)
                                .fill(Color.white.opacity(0.5))
                                .frame(width: 350, height: 400)
                            
                            VStack {
                                Text("トレンド")
                                    .font(.body.bold())
                                    .foregroundColor(.gray)
                                    .padding()
                                    .frame(width: 350, alignment: .leading)
                                Text("注目のドラマ・映画・\n番組・スポーツ観戦")
                                    .font(.title.bold())
                                    .foregroundColor(.black)
                                
                                ForEach(apps) { app in
                                    AppCardView(
                                        imageName: app.imageName,
                                        title: app.title,
                                        description: app.description,
                                        textColor: app.textColor,
                                        backgroundColor: app.backgroundColor
                                    )
                                }
                            }
                        }
                    }
                    .padding(.bottom, 200)
                }
            }
            
            // ボトムナビゲーションバー
            VStack {
                Spacer()
                BottomBarView()
            }
        }
    }
}

#Preview {
    ContentView()
}

モーダルの遷移先

//
//  ModalView.swift
//  MarchHomework
//
//  Created by tomohide katagiri on 2025/03/23.
//

import SwiftUI

struct ModalView: View {
    @Binding var isPresented: Bool
    @GestureState private var dragOffset: CGSize = .zero
    
    var body: some View {
        VStack{
            Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.white)
        .edgesIgnoringSafeArea(.all)
        .contentShape(Rectangle())
        .gesture(
            DragGesture()
                .updating($dragOffset){ value, state, _ in
                    state = value.translation
                }
                .onEnded{ value in
                    if value.translation.height > 100 {
                        isPresented = false
                    }
                }
        )
    }
}

#Preview {
    StatefulPreviewWrapper(true) { isPresented in
        ModalView(isPresented: isPresented)
    }
}



ようわからんビュー

import SwiftUI

struct StatefulPreviewWrapper<Value, Content: View>: View {
    @State var value: Value
    var content: (Binding<Value>) -> Content
    
    init(_ value: Value, @ViewBuilder content: @escaping (Binding<Value>) -> Content) {
        self._value = State(initialValue: value)
        self.content = content
    }
    
    var body: some View {
        content($value)
    }
}

入手ボタン

import SwiftUI

struct PurchaseButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.system(size: 20).weight(.bold))
            .foregroundColor(.white)
            .padding(.horizontal, 15)
            .padding(8)
            .frame(minWidth: 44, minHeight: 44)
            .background(
                RoundedRectangle(cornerRadius: 30)
                    .fill(Color.gray.opacity(configuration.isPressed ? 0.4 : 0.6))
            )
    }
}

struct PurchaseButtonView: View {
    var action: () -> Void = {}
    
    var body: some View {
        Button(action: action) {
            Text("入手")
        }
        .buttonStyle(PurchaseButtonStyle())
    }
}

#Preview {
    PurchaseButtonView()
}


スポーツザリガニスポーツザリガニ

3/23
今日は外出予定が長かったと、昨日寝れてないので早めに寝たいので復習のインプットだけ。
昨日ガーっと ChatGPTに聞いたのをコピーして進めてしまったので理解できてないところを中心に。

・これなに?

.updating($dragOffset) { value, state, _ in
                    state = value.translation

→DragGesture の途中経過をリアルタイムで追いかけるためのコード。

・_ inってよく出てくるけどなんだ?
→_ は「使わない引数」を表す特別な記号
in はクロージャの「ここから処理を書くよ!」の区切り

・クロージャって何?
→あとで実行できるコードのかたまり。ポケットに入れとける関数。

{ (引数) -> 戻り値の型 in
    // 実行される処理
}

・関数との違い
→名前がなくてもOK、funcで定義しないで{} で囲む、その場だけで使うことが多い

・なんでクロージャって名前なの?
→「囲い込む(close)」っていう意味から来てる。外のスコープを 閉じ込めてる。
クロージャは「外の変数を閉じ込めて(=記憶して)使えるコードのかたまり」だから「クロージャ(閉じ込めるもの)」って名前になった。

クロージャはスコープごと包んで持ち運べるレトルトカレー
その場で食べてもいいし
冷凍して(変数に入れて)
あとでチンして(呼び出して)
元の味(変数)そのままで楽しめる!

・スコープってなんだ?
→コードの「ナワバリ」や「部屋」
グローバルスコープ…ファイル全体で使える ファイルの一番外側に書いた変数など
ローカルスコープ…関数やif文などの中だけで使える。funcの中、ifの中、forの中など
クロージャのスコープ…クロージャの中でしか見えない { ... } in の中で定義した変数な

・$〜ってやつ、なんだ?
→$ は「Binding(バインディング)」を作る記号。
変数そのものじゃなくて、“その変数とつながる”ものを渡したいとき」に使う。

・「コードは動くけど、理解はついてきてない気がする」
「写してるだけで、本当に身についてるのかわからない」

→次にコードを写すときは、1行ごとに「これは何をしてるんだっけ?」って小さく自分に解説してみるといい。

・昨日書いたラッパービューの解説

struct StatefulPreviewWrapper<Value, Content: View>: View {
    @State var value: Value
    var content: (Binding<Value>) -> Content
    
    init(_ value: Value, @ViewBuilder content: @escaping (Binding<Value>) -> Content) {
        self._value = State(initialValue: value)
        self.content = content
    }
    
    var body: some View {
        content($value)
    }
}

→Previewで @Binding を使いたいときに使う、便利なラッパービュー。
ふつう、@Binding は他のViewから $変数 を渡さないと使えない。
でもPreviewって @State 使えないし、$ 渡す元がない。
StatefulPreviewWrapper を使えば、
Previewでも仮のBinding値を使ってViewの動作を試せるようになる。

◾️明日やること
遷移先のページの作成。

スポーツザリガニスポーツザリガニ

3/24
◾️今日やったこと
遷移先ページの作成。

import SwiftUI

struct ModalView: View {
    @Binding var isPresented: Bool
    @GestureState private var dragOffset: CGSize = .zero
    
    var body: some View {
        ScrollView {
            VStack{
                ZStack{
                    Image("neko1")
                        .resizable()
                        .scaledToFill()
                        .frame(height: UIScreen.main.bounds.height / 2)
                        .ignoresSafeArea(edges: .top)
                        .clipped()
                    
                    HStack {
                        Text("木 15:00")
                            .padding(4)
                            .font(.body)
                            .foregroundColor(.white)
                            .shadow(color: .black.opacity(0.8), radius: 3, x: 0, y: 2)
                            .background(Color.blue.opacity(0.6))
                            .clipShape(RoundedRectangle(cornerRadius: 10))
                            .padding(.bottom, 350)
                            .padding()
                        
                        Spacer()
                        
                        Button(action:{}) {
                            Image(systemName: "xmark.circle.fill")
                                .font(.system(size: 35, weight: .bold))
                                .foregroundColor(.white)
                                .padding(.bottom, 380)
                                .padding()
                        }
                    }
                    
                    
                    HStack {
                        Text("基本を知る")
                            .font(.body.bold())
                            .foregroundColor(.white)
                            .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                            .padding()
                        Spacer()
                    }
                    .frame(width: 350)
                    
                    HStack {
                        Text("いざという時に\n行動できる情報を届ける")
                            .font(.title.bold())
                            .foregroundColor(.white)
                            .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                            .padding(.top, 100)
                            .padding()
                        Spacer()
                    }
                    AppCardView(imageName: "neko2", title: "nekoo.防災訓練", description: "地震や災害などの災害情報\nを通知", textColor: .white, backgroundColor: .gray.opacity(0.5))
                        .padding(.top, 300)
                }
                
                Spacer()
                
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color(.systemBackground))
            }
            VStack{
                Text("ネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャンネコチャン")
                    .font(.title2)
                    .foregroundColor(.gray)
                    .padding()
                
            }
            
            
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.white)
            .edgesIgnoringSafeArea(.all)
            .contentShape(Rectangle())
            .gesture(
                DragGesture()
                    .updating($dragOffset){ value, state, _ in
                        state = value.translation
                    }
                    .onEnded{ value in
                        if value.translation.height > 100 {
                            isPresented = false
                        }
                    }
            )
        }
    }
}

#Preview {
    StatefulPreviewWrapper(true) { isPresented in
        ModalView(isPresented: isPresented)
    }
}

発生している問題、なおしたいところ
・スクロールビューにしたせいか、下スワイプで閉じれなくなった。
・.ignoresSafeArea(edges: .top)でセーフエリア無視してほしいのに上に余白がある。
・AppCardViewでコンポーネント化したやつを再利用しようと思ったけど、この画面では横幅を画面いっぱいに変更したい。

◾️明日やること
これらをChatGPTに聞いて、理解しながら修正。

今日作った画面

目指してるレイアウト

スポーツザリガニスポーツザリガニ

昨日やりかけで寝落ちして今日午前中にやった部分

・・スクロールビューでも下スワイプで閉じれるように、透明のビューを重ねてそっちで閉じるようにした
・.ignoresSafeArea(edges: .top)を一番外にもってきて修正
・AppCardViewでコンポーネント化したやつを再利用するために、幅と角丸具合をプロパティにして変更できるようにした

import SwiftUI

struct ModalView: View {
    @Binding var isPresented: Bool
    @GestureState private var dragOffset: CGSize = .zero
    
    var body: some View {
        ZStack(alignment: .top) {
            ScrollView(showsIndicators: false) {
                VStack(spacing: 0) {
                    ZStack(alignment: .top) {
                        Image("neko1")
                            .resizable()
                            .scaledToFill()
                            .frame(width: UIScreen.main.bounds.width,
                                   height: UIScreen.main.bounds.height / 2)
                            .clipped()
                        
                        VStack {
                            HStack {
                                Text("木 15:00")
                                    .font(.body)
                                    .foregroundColor(.white)
                                    .padding(6)
                                    .background(Color.blue.opacity(0.6))
                                    .clipShape(RoundedRectangle(cornerRadius: 10))
                                    .shadow(radius: 2, y: 1)
                                
                                Spacer()
                                
                                Button(action: { isPresented = false }) {
                                    Image(systemName: "xmark.circle.fill")
                                        .font(.system(size: 35, weight: .bold))
                                        .foregroundColor(.white)
                                }
                            }
                            .padding(.horizontal)
                            .padding(.top, 50)
                            
                            Spacer()
                            
                            VStack(alignment: .leading, spacing: 8) {
                                Text("基本を知る")
                                    .font(.body.bold())
                                    .foregroundColor(.white)
                                
                                Text("いざという時に\n行動できる情報を届ける")
                                    .font(.title.bold())
                                    .foregroundColor(.white)
                            }
                            .padding(.horizontal)
                            
                            AppCardView(imageName: "neko2",
                                        title: "nekoo.防災訓練",
                                        description: "地震や災害などの災害情報\nを通知",
                                        textColor: .white,
                                        backgroundColor: .gray.opacity(0.5),
                            cardWidth: UIScreen.main.bounds.width,
                                        cardRadius:0)
                            .frame(maxWidth: .infinity)
                            .padding(.horizontal)
                        }
                        .frame(height: UIScreen.main.bounds.height / 2)
                    }
                    
                    VStack {
                        Text("ネコチャン〜")
                            .font(.title2)
                            .foregroundColor(.gray)
                            .padding()
                        Spacer()
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.white)
                }
            } // ← ScrollViewの終了位置(重要!)
            
            // ドラッグジェスチャ用の透明ビュー
            Color.clear
                .frame(height: 200)
                .contentShape(Rectangle())
                .gesture(
                    DragGesture()
                        .updating($dragOffset) { value, state, _ in
                            state = value.translation
                        }
                        .onEnded { value in
                            if value.translation.height > 100 {
                                isPresented = false
                            }
                        }
                )
        }
        .ignoresSafeArea(edges: .top)
    }
}

#Preview {
    StatefulPreviewWrapper(true) { isPresented in
        ModalView(isPresented: isPresented)
    }
}

・幅と角丸をプロパティで調整できるようにしたコンポーネント

import SwiftUI

struct AppCardView: View {
    let imageName: String
    let title: String
    let description: String
    let textColor: Color
    let backgroundColor: Color
    var cardWidth: CGFloat? = 350
    var cardRadius: CGFloat = 20
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: cardRadius)
                .fill(backgroundColor)
                .frame(width: cardWidth, height: 90)
            
            HStack(spacing: 0) {
                Image(imageName)
                    .resizable()
                    .scaledToFill()
                    .frame(width: 60, height: 60)
                    .cornerRadius(10)
                    .padding(.trailing, 15)
                
                VStack(alignment: .leading, spacing: 5) {
                    Text(title)
                        .font(.headline.bold())
                        .foregroundColor(textColor)
                    
                    Text(description)
                        .font(.caption.bold())
                        .foregroundColor(textColor)
                }
                .padding(.trailing, 20)

                PurchaseButtonView {
                    print("\(title) の入手ボタンが押された!")
                }
            }
        }
    }
}

今晩やること
×ボタンでもどれるようにする

スポーツザリガニスポーツザリガニ

3/26
Appstoreの「画面いっぱいにモーダル遷移した先の画面で、画面が一番上にスクロールされている状態で下スワイプorボタン押下どちらでも元の画面に戻る」を真似たいんだけどうまくいかなかった日。
・透明なビューかぶせちゃうとボタンにかぶさってボタンが死ぬ
・allowsHitTesting(false)だとスワイプでの動作が死ぬ
・gestureをScrollView自体に付けてもスワイプでの動作が死ぬ
・透明なビューをボタンに被らない位置だけに配置してもなんかうまくいかない

どうやら根本的な原因は「ScrollViewがデフォルトで縦方向スクロールを持っているため、
縦のDragGestureがScrollViewのスクロールと干渉して、ジェスチャが認識されてない」点にあるっぽい

import SwiftUI

struct ModalView: View {
    @Binding var isPresented: Bool
    @GestureState private var dragOffset: CGSize = .zero
    
    var body: some View {
        ZStack(alignment: .top) {
            ScrollView(showsIndicators: false) {
                VStack(spacing: 0) {
                    ZStack(alignment: .top) {
                        Image("neko1")
                            .resizable()
                            .scaledToFill()
                            .frame(width: UIScreen.main.bounds.width,
                                   height: UIScreen.main.bounds.height / 2)
                            .clipped()
                        
                        VStack {
                            HStack {
                                Spacer()
                                Button(action: { isPresented = false }) {
                                    Image(systemName: "xmark.circle.fill")
                                        .font(.system(size: 35, weight: .bold))
                                        .foregroundColor(.white)
                                }
                            }
                            .padding(.horizontal)
                            .padding(.top, 50)
                            
                            Spacer()
                            
                            VStack(alignment: .leading, spacing: 8) {
                                Text("基本を知る")
                                    .font(.body.bold())
                                    .foregroundColor(.white)
                                
                                Text("いざという時に\n行動できる情報を届ける")
                                    .font(.title.bold())
                                    .foregroundColor(.white)
                            }
                            .padding(.horizontal)
                            
                            AppCardView(imageName: "neko2",
                                        title: "nekoo.防災訓練",
                                        description: "地震や災害などの災害情報\nを通知",
                                        textColor: .white,
                                        backgroundColor: .gray.opacity(0.5),
                                        cardWidth: UIScreen.main.bounds.width,
                                        cardRadius: 0)
                        }
                        .frame(height: UIScreen.main.bounds.height / 2)
                    }
                    
                    VStack {
                        Text("ネコチャンネコチャンネコチャン...")
                            .font(.title2)
                            .foregroundColor(.gray)
                            .padding()
                        Spacer()
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.white)
                }
            }
            
            // 🌟ここがポイント🌟
            VStack {
                Rectangle()
                    .fill(Color.clear)
                    .frame(height: 40) // ←ここを小さく(40)してボタンと干渉させない
                    .contentShape(Rectangle())
                    .gesture(
                        DragGesture()
                            .updating($dragOffset) { value, state, _ in
                                state = value.translation
                            }
                            .onEnded { value in
                                if value.translation.height > 100 {
                                    isPresented = false
                                }
                            }
                    )
                
                Spacer() // 上の透明領域以外は無関係のためSpacerで埋める
            }
        }
        .ignoresSafeArea(edges: .top)
    }
}

#Preview {
    StatefulPreviewWrapper(true) { isPresented in
        ModalView(isPresented: isPresented)
    }
}

スポーツザリガニスポーツザリガニ

3/29
3/27~28は、雨キャンプからの道具背負って直接出社だったので二日間の体力ゲージが0でした。
今日の業務後泥のように寝て一旦回復。

◾️今日やったこと
・画面上部でドラッグしたら下の画面に戻る処理が実装できた。
当初はスクロール位置を監視して、画面が最上部で100px以上下にドラッグしたら前の画面に戻るみたいなやり方でやったけど、やはりScrollViewとの噛み合わせが悪くてうごかなかった。

▶︎SwiftUIのデフォルトのScrollViewは、上端で下方向に引っ張ってもドラッグイベントを検知しない仕様。
▶︎スクロールが最上部にいる時に、さらに下に引っ張る動作は、スクロールビューの「バウンス(跳ね返り)効果」として扱われて、通常のDragGestureでは拾えない仕組み

最終的に、ScrollViewの上に透明な「80pxの高さのドラッグ検知エリア」を用意して、下方向へのドラッグを拾うかたちでうまくいきました。
(AppStoreみたいに、画面が引っ張られるアニメーションはつかないためちょっとわかりづらいけど)

◾️明日やること
ContentViewのボタンアイコンを押したらリストビューでアカウント情報が書いてあるページをつくる
その元気がなければ各ビューの配置の調整とかをする

import SwiftUI

struct ModalView: View {
    @Binding var isPresented: Bool
    @GestureState private var dragOffset: CGSize = .zero
    
    var body: some View {
        ZStack(alignment: .top) {
            ScrollView(showsIndicators: false) {
                VStack(spacing: 0) {
                    Image("neko1")
                        .resizable()
                        .scaledToFill()
                        .frame(height: UIScreen.main.bounds.height / 2)
                        .clipped()
                    
                    VStack {
                        Text("ネコチャンネコチャンネコチャン")
                            .font(.title2)
                            .foregroundColor(.gray)
                            .padding()
                        
                        Spacer().frame(height: 800)
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.white)
                }
            }
            
            HStack {
                Spacer()
                Button(action: { isPresented = false }) {
                    Image(systemName: "xmark.circle.fill")
                        .font(.system(size: 35, weight: .bold))
                        .foregroundColor(.white)
                }
            }
            .padding(.horizontal)
            .padding(.top, 50)
            .zIndex(2) // ボタンを前面に出す
            
            //上部に透明なドラッグエリアを設置する
            Rectangle()
                .fill(Color.clear)
                .frame(height: 80)
                .contentShape(Rectangle())
                .gesture(
                    DragGesture()
                        .updating($dragOffset) { value, state, _ in
                            state = value.translation
                        }
                    // 80px以上下ドラッグで閉じる処理
                        .onEnded { value in
                            if value.translation.height > 80 {
                                isPresented = false
                            }
                        }
                )
                .zIndex(1) //透明エリアを後ろに下げる
        }
        .ignoresSafeArea(edges: .top)
    }
}

#Preview {
    StatefulPreviewWrapper(true) { isPresented in
        ModalView(isPresented: isPresented)
    }
}
スポーツザリガニスポーツザリガニ

3/30

◾️今日やったこと
・やっぱり引っ張った時に画面がちっちゃくなるエフェクトほしいと思って追加。
・追加したら四角いままちいさくなるのがダサかったので角丸も追加。
「トレンド」の中の配置調整。

学んだこと
スクロールビューの「バウンス効果」ってなに?
バウンス効果とは、スクロールビュー(ScrollViewやリストなど)が一番上か一番下にいる時に、ユーザーがさらにスクロールしようとすると、一時的にコンテンツが少しだけビューの端から離れて、跳ね返るように戻ってくる現象のこと。スクロールビューにデフォルトで設定されてる。
・「引っ張ったら画面全体が小さくなる現象」とは別もの。
画面全体が引っ張ったときに「縮小」される効果(UIが小さくなって奥に引っ込むようなアニメーション)は、『バウンス効果』とは呼ばない。
それは『プルダウンのインタラクション』や『引っ張り時のアニメーション』と呼ばれる別の効果。

メイン画面

import SwiftUI

//AppData の定義
struct AppData: Identifiable {
    let id = UUID()
    let imageName: String
    let title: String
    let description: String
    let textColor: Color
    let backgroundColor: Color
}

//BottomButton の定義
struct BottomButton: Identifiable {
    let id = UUID()
    let title: String
    let systemImageName: String
}


//アプリデータ
let apps: [AppData] = [
    AppData(imageName: "neko2", title: "MeowTube", description: "公式アプリで動画と音楽", textColor: .black, backgroundColor: .white),
    AppData(imageName: "neko2", title: "TikCats", description: "もっと猫を好きになる。", textColor: .black, backgroundColor: .white),
    AppData(imageName: "neko2", title: "Ben cat Meaw", description: "Originals,movies,cats", textColor: .black, backgroundColor: .white)
]

// ボトムバーのボタンデータ
let bottomButtons: [BottomButton] = [
    BottomButton(title: "Today", systemImageName:"text.rectangle.page"),
    BottomButton(title: "ゲーム", systemImageName: "rugbyball"),
    BottomButton(title: "アプリ", systemImageName: "square.3.layers.3d"),
    BottomButton(title: "Arcade", systemImageName: "arcade.stick"),
    BottomButton(title: "検索", systemImageName: "magnifyingglass")
]


struct ContentView: View {
    @State private var isSheetPresented = false
    var body: some View {
        ZStack {
            Color.gray.opacity(0.2)
                .edgesIgnoringSafeArea(.all)
            
            ScrollView {
                VStack {
                    // ヘッダー部分(日付&アイコン)
                    HStack {
                        Text("Today")
                            .font(.system(size: 40).weight(.heavy))
                            .padding()
                        Text(Date().formatted(.dateTime.month().day().locale(Locale(identifier: "ja_JP"))))
                            .font(.system(size: 25).weight(.bold))
                            .foregroundColor(.gray)
                            .padding(.top, 10)
                        Spacer()
                        Image(systemName:"person.crop.circle.fill")
                            .font(.system(size: 40))
                            .foregroundColor(.gray)
                            .padding()
                            .padding(.top, 10)
                    }
                    
                    // メインコンテンツ(スクロール部分)
                    ZStack {
                        Button(action: {isSheetPresented = true}) {
                            Image("neko1")
                                .scaledToFit()
                                .frame(width: 350, height: 400)
                                .cornerRadius(30)
                        }
                        .fullScreenCover(isPresented: $isSheetPresented){ ModalView(isPresented: $isSheetPresented)
                                .presentationDetents([.large])
                                
                        }
                        
                        HStack {
                            Text("基本を知る")
                                .font(.body.bold())
                                .foregroundColor(.white)
                                .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                .padding()
                            Spacer()
                        }
                        .frame(width: 350)
                        
                        HStack {
                            Text("いざという時に\n行動できる情報を届ける")
                                .font(.title.bold())
                                .foregroundColor(.white)
                                .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                .padding(.top, 100)
                                .padding()
                            Spacer()
                        }
                        .frame(width: 350)
                        
                        VStack {
                            Spacer()
                            ZStack {
                                AppCardView(imageName: "neko2", title: "nekoo.防災訓練", description: "地震や災害などの災害情報\nを通知", textColor: .white, backgroundColor: .gray.opacity(0.5))
                            }
                        }
                    }
                    // 広告ブロック
                    ZStack {
                        Image("neko3")
                            .resizable()
                            .scaledToFill()
                            .frame(width: 350, height: 150)
                            .offset(y: 80)
                            .clipped()
                            .cornerRadius(30)
                            .padding()
                        
                        HStack {
                            VStack {
                                Text("SDニャンダム")
                                    .font(.headline.bold())
                                    .foregroundColor(.white)
                                    .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2)
                                    .padding(.trailing, 130)
                                    .padding(.top, 80)
                                HStack {
                                    Text("広告")
                                        .padding(4)
                                        .font(.callout)
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.8), radius: 3, x: 0, y: 2)
                                        .background(Color.blue.opacity(0.6))
                                        .clipShape(RoundedRectangle(cornerRadius: 10))
                                        .padding(.top, -15)
                                        .padding(.trailing, -20)
                                    Text("近日公開")
                                        .padding(8)
                                        .padding(.horizontal, 5)
                                        .font(.callout.bold())
                                        .foregroundColor(.white)
                                        .shadow(color: .black.opacity(0.8), radius: 3, x: 0, y: 2)
                                        .padding(.top, -15)
                                        .padding(.trailing, 120)
                                }
                            }
                            PurchaseButtonView {
                                print("広告ブロックの入手ボタンが押された!")
                            }
                            .padding(.top, 60)
                        }
                    }
                    
                    // トレンドアプリのリスト
                    VStack(spacing: 0) {
                        ZStack {
                            RoundedRectangle(cornerRadius: 20)
                                .fill(Color.white)
                                .frame(width: 350, height: 400)
                            
                            VStack {
                                VStack(alignment: .leading, spacing: 8) {
                                    Text("トレンド")
                                        .font(.body.bold())
                                        .foregroundColor(.gray)
                                        .padding(.top, 30)
                                    
                                    Text("注目のドラマ・映画・\n番組・スポーツ観戦")
                                        .font(.title.bold())
                                        .foregroundColor(.black)
                                }
                                .padding()
                                .frame(width: 350, alignment: .leading)
                                
                                ForEach(apps) { app in
                                    AppCardView(
                                        imageName: app.imageName,
                                        title: app.title,
                                        description: app.description,
                                        textColor: app.textColor,
                                        backgroundColor: app.backgroundColor
                                    )
                                }
                            }
                        }
                    }
                    .padding(.bottom, 200)
                }
            }
            
            // ボトムナビゲーションバー
            VStack {
                Spacer()
                BottomBarView()
            }
        }
    }
}

#Preview {
    ContentView()
}

モーダル画面

import SwiftUI

struct ModalView: View {
    @Binding var isPresented: Bool
    @GestureState private var dragOffset: CGSize = .zero
    @State private var viewScale: CGFloat = 1.0
    @State private var viewCornerRadius: CGFloat = 0
    
    var body: some View {
        ZStack(alignment: .top) {
            ScrollView(showsIndicators: false) {
                VStack(spacing: 0) {
                    Image("neko1")
                        .resizable()
                        .scaledToFill()
                        .frame(height: UIScreen.main.bounds.height / 2)
                        .clipped()
                    
                    AppCardView(imageName: "neko2",
                                title: "nekoo.防災訓練",
                                description: "地震や災害などの災害情報\nを通知",
                                textColor: .white,
                                backgroundColor: .gray.opacity(0.4),
                                cardWidth: UIScreen.main.bounds.width,
                                cardRadius: 0)
                    
                    VStack {
                        Text("ネコチャンネコチャンネコチャン。")
                            .font(.title2)
                            .foregroundColor(.gray)
                            .padding()
                        
                        Spacer().frame(height: 800)
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.white)
                }
            }
            
            HStack {
                Spacer()
                Button(action: { isPresented = false }) {
                    Image(systemName: "xmark.circle.fill")
                        .font(.system(size: 35, weight: .bold))
                        .foregroundColor(.white)
                }
            }
            .padding(.horizontal)
            .padding(.top, 50)
            .zIndex(2)
            
            Rectangle()
                .fill(Color.clear)
                .frame(height: 200)
                .frame(maxWidth: .infinity)
                .contentShape(Rectangle())
                .gesture(
                    DragGesture()
                        .updating($dragOffset) { value, state, _ in
                            state = value.translation
                        }
                        .onChanged { value in
                            updateScale(with: value.translation.height)
                        }
                        .onEnded { value in
                            if value.translation.height > 80 {
                                isPresented = false
                            } else {
                                withAnimation(.spring()) {
                                    viewScale = 1.0
                                    viewCornerRadius = 0
                                }
                            }
                        }
                )
                .zIndex(1)
        }
        .clipShape(RoundedRectangle(cornerRadius: viewCornerRadius))
        .scaleEffect(viewScale)
        .animation(.interactiveSpring(), value: viewScale)
        .animation(.interactiveSpring(), value: viewCornerRadius)
        .ignoresSafeArea(edges: .top)
    }
    
    private func updateScale(with dragHeight: CGFloat) {
        let dragLimit: CGFloat = 200
        let minScale: CGFloat = 0.2
        
        if dragHeight > 0 {
            let progress = min(dragHeight / dragLimit, 1.0)
            viewScale = max(1.0 - progress * 0.2, minScale)
            viewCornerRadius = progress * 30
        } else {
            viewScale = 1.0
            viewCornerRadius = 0
        }
    }
}

#Preview {
    StatefulPreviewWrapper(true) { isPresented in
        ModalView(isPresented: isPresented)
    }
}

ログインするとコメントできます