見出し画像

iPhone アプリを自分でつくる 21.


今回の内容: オブジェクトとリスト3.


今回はリストから詳細情報への画面遷移を試してみます。



前回のコードの続きとなります。
前回の最後にコード全文がありますので、今回から始める方は前回のコードをコピペして準備してください。

また今回も最後にコードを載せておきますので、お時間のない方はそちらをご確認ください。



イメージ

全国の城のリストは作成できました。
そのリストをタップすると城の詳細な情報が表れるようにしたいです。


NavigationList


SwiftUI で画面遷移する方法にはいろいろとあります。
リストをタップすると横から次の画面がでてくるもの、
画面の下方向からスッと入力画面が現れるもの、
画面が全体に切り替わったり、半分だけ変わったりするもの、
エラーを促すために小さなウィンドウがパッと現れるもの、
タップするとお知らせとして小さなポップアップが現れるもの、など。

状況に応じて適当なものを使い分けていきます。
今回は詳細情報の画面に遷移する方法として NavigationStack - NavigationList を使用します。

このNavigationList を使用する方法には大きく分けて 2通りあります。

[1] NavigationList { 画面遷移先view } label: { タップするview }

[2] {   NavigationList { タップするview,  value: 画面遷移先の }
     } .navigationDestination(for: 型.self) { item in 
                画面遷移先view
     }

[1] はiOS15までの基本的な使用 NavigationView - NavigationList として使われてきた方法です。とてもシンプルで使い勝手も良いです。
Buttonボタンと同じような使い勝手です。
ボタンでは Button { アクション } label: { ボタンview }   でしたが、これは
NavigationList { 遷移先view } label: { リスト表示view } のように難なく使用できます。

[2] は新しい方法です。リストがタップされると .navigationDestination がその型
を読み取って画面遷移が行われます。

今まで簡単な方法でできていたのに、なにゆえ、わざわざ[2]のように文字も多い、型まで明記するような、理解しにくい方法を新たに設定したのか? なにか問題があったのか!

それはLazy loading するためです。
コードを見てみると [1]の方法は、リスト表示view と遷移先view がひとつになっているのでイニシャライズが同時に行われてしまいます。
なので、詳細画面に大きな写真をそれぞれ持たせた場合、リストが作られる数だけ裏で大きな写真も多数表示させることになります。
[2]ではリストがタップされて初めて.navigationDestination が起動して詳細画面を作って表示する作業が行われます。Lazy なメソッド(view modifier)なのです。

以前に比べてシステムの性能も上がり、必要な時だけ呼び出しても表示が遅延するようなことは無くなったからこのような方法が作られるようになってきたのでしょうか。

今回試しているような文字だけのアプリではどちらにしても影響はなさそうですが、例えばリストに写真を入れる場合などは、LazyVStack やList や、[2]の navigationDestination を検討することになるかと思います。

今回はどちらも試してみます。


まずは[1]で行なってみます。
ボタン作成の要領で HStack をlabel で囲んで NavigationLink を設定します。
下の例ではまだ 遷移先 view を入れていない状態です。

                    ForEach(filteredCastles) { castle in
                        NavigationLink {
                                           // 遷移先はまだ設定していない
                        } label: {
                            HStack {
                                Text(castle.name)
                                    .padding(.leading, 20)
                                Spacer()
                                Text(castle.place)
                                    .padding(.trailing, 20)
                            }
                            .padding(.vertical, 10)
                            .background(.gray.opacity(0.1))
                            .clipShape(RoundedRectangle(cornerRadius: 10))
                            
                        }
                    }

NavigationLink にまだ 遷移先 view を設定していないですがリストの文字が青で表示されてタップを待つ状態になりました。下図①
タップすると空白画面に遷移しますが戻るボタンが自動で設置されます。下図②
試しに遷移先に城の名前を大きく表示するよう設定します。

                    ForEach(filteredCastles) { castle in
                        NavigationLink {
                            Text(castle.name)
                                .font(.system(size: 80).weight(.bold))
                        } label: {
                            HStack {

そうすると遷移先に大きく城の名前が表示されました。③


navigationDestination

次は[2]の方法を試してみます。
まずはいったん最初のコードまで戻ります。

// 元々のコード
                    ForEach(filteredCastles) { castle in
                        
                        HStack {
                            Text(castle.name)
                                .padding(.leading, 20)
                            Spacer()
                            Text(castle.place)
                                .padding(.trailing, 20)
                        }
                        .padding(.vertical, 10)
                        .background(.gray.opacity(0.1))
                        .clipShape(RoundedRectangle(cornerRadius: 10))
                        
                    }

この方法の場合、遷移先は一番下に書いてある navigationDestination(for: 〜 ) { castle in 〜 } となります。

                    ForEach(filteredCastles) { castle in
                        NavigationLink(value: castle) {
                            HStack {
                                Text(castle.name)
                                    .padding(.leading, 20)
                                Spacer()
                                Text(castle.place)
                                    .padding(.trailing, 20)
                            }
                            .padding(.vertical, 10)
                            .background(.gray.opacity(0.1))
                            .clipShape(RoundedRectangle(cornerRadius: 10))
                            
                        }
                        .navigationDestination(for: Castle.self) { castle in
                            Text(castle.name)
                        }                        
                    }

ここで表示を待ってみると Hashable を要求されました
ForEachくんと違って厳格に Hashable を宣言しないといけないようです。
Castle 型に Hashable をつけるとエラー表示が消えました

struct Castle: Identifiable, Hashable {
    let id: Int
    let name:String
    let place: String
    let nTreasure: Bool
}


リストが青で表示されてタップする準備ができました。③
タップしてみると画面遷移しました。④

この[2] の方法で NavigationLink(value: castle) {  は Castle型のインスタンス(イニシャライズされて使えるようになった物: castle)を持ちます。そして .navigationDestination は (for: Castle.self) として Castle型に合致しているか、を確認してcastle インスタンスを受け取ります。
* Castle.self のself はCastleの型自体を指すものです。
受け取る型によって Int.self や String.self などを設定します。


注意
.navigationDestination のQuick Help を見てみると、これを List とか LazyVStack などの lazy container レイジーコンテナーの中には入れないように注意してください、とあります。
つまり List { { 〜 }.navigationDestination } のようにならないように、ということです。
こうなると親Lazy の中に子Lazy が入ってしまって、どうも働かなくなってしまう(NavigationStack から見えなくなってしまうから) らしいです。
つまり子供は育つ環境が大事だということですね!

なおつけるときは、LazyVStack {   }.navigationDestivation のように外から包む形にしてください、とあります。


 .navigationDestination の中にある遷移先のview に情報を加えてみます。

                        .navigationDestination(for: Castle.self) { castle in
                            VStack {
                                Text(castle.name)
                                Text(castle.place)
                                Text(castle.nTreasure ? "国宝" : "重要文化財")
                            }
                        }

正しく情報を表示することができそうです。



ではCastle 構造体をもう少し充実させましょう。
プロパティを増やします。
エラーが出ますがびっくりしないでくださいね。

struct Castle: Identifiable, Hashable {
    let id: Int
    let name:String
    let place: String
    let nTreasure: Bool
    let builtBy: [String]
    let builtYr: Int
    let rmks: String
}

(私ももう大丈夫です)

builtBy は築城者を記入していますが、1人のときと2人のときがあるので配列[String]で設定しました。
builtYr は築城年を記入しています。
rmks は 城の情報を記入しています。


AllCastles 構造体をこちらに変更します。右上のアイコンでコピペしてください。
とても文字が多くなりました

struct AllCastles {
    let items = [
        Castle(id: 1, name: "弘前城", place: "青森", nTreasure: false, builtBy: ["津軽為信", "津軽信枚"], builtYr: 1611, rmks:  "独立式層塔型3重3階の天守で、現在の現存天守の中では日本最北かつ最東端の地にある。1627年(寛永4年)に落雷により天守を焼失した後、1810年(文化7年)に幕府にはばかって名目上櫓として3重3階層塔型構造で新築したもので、「御三階櫓」と名付けられた。城外側にあたる東・南面には切妻破風を2重に重ねて出窓や出張を設け、窓の代わりに矢狭間を用いた構えであるが、城内側にあたる西・北面には天守建築の特徴の一つである破風がなく、連続した窓があけられている。また、凍結に対応するために鯱や屋根は銅瓦(木型の上に銅板を貼り付けているもの)が用いられている。[wikipedia]"),
        Castle(id: 2, name: "松本城", place: "長野", nTreasure: true, builtBy: ["石川数正", "石川康長"], builtYr: 1593, rmks: "層塔型5重6階の大天守と3重4階の乾小天守、2重の辰巳附櫓と月見櫓を付属させた複合連結式の天守で、「現存12天守」の中では唯一平城の天守である。破風が少なく、黒塗りの下見板がめぐらされているため、漆黒で簡素な外観であるが、複合連結式であるため見る角度によって異なる印象の意匠を見ることができる。[wikipedia]"),
        Castle(id: 3, name: "丸岡城", place: "福井", nTreasure: false, builtBy: ["柴田勝豊"], builtYr: 1576, rmks: "独立式望楼型2重3階の天守で、最古の現存天守とする説もある。1948年(昭和23年)の福井地震により倒壊したが、元の古材を80パーセント近く使用して1955年(昭和30年)に再建された。飾り外廻縁と高欄を有し、「現存12天守」の中では採光がよく室内が明るい点や、凍結で割れてしまう粘土瓦の代わりに石瓦が葺かれている(石の鯱も展示)ことなどの特徴がある。[wikipedia]"),
        Castle(id: 4, name: "犬山城", place: "愛知", nTreasure: true, builtBy: ["織田広近"] , builtYr: 1469, rmks: "複合式望楼型3重4階地下2階の天守で、大入母屋屋根の建築の上に外廻縁側を突出させた小規模な望楼を上げた形状は丸岡城天守と同様である。この天守は最上階に実用的な外廻縁と高欄が付けられ、華頭窓も付けられているが、実際は窓ではなく装飾である。小屋裏となる3階にも唐破風出窓を設けるなどの採光が考慮されている。[wikipedia]"),
        Castle(id: 5, name: "彦根城", place: "滋賀", nTreasure: true, builtBy: ["井伊直継"], builtYr: 1622, rmks: "複合式望楼型3重3階地下1階の天守で、幕府の普請(天下普請)による。飾り外廻縁と高欄を持ち、切妻破風、入母屋破風、千鳥破風、唐破風が組み合った、複雑な構造美と輪郭の荘厳な景観の意匠となっている。また、文禄・慶長の役の際に朝鮮半島に造られた倭城にも見られる「登り石垣(竪石垣)」や大名庭園「玄宮園」も現存する。[wikipedia]"),
        Castle(id: 6, name: "姫路城", place: "兵庫", nTreasure: true, builtBy: ["赤松貞範"], builtYr: 1346, rmks: "望楼型5重6階地下1階の大天守と3重の小天守3基を2重の多聞櫓で連結させた連立式の天守で、日本国内最大の現存天守である。白漆喰で塗られた白亜の外壁と屋根や破風の構成美の上、見る方向により異なった趣となる連立式であり、また桃山後期から江戸初期当時の作事(建築)の技を現代に伝える代表的な城郭と言われている。また、「現存12天守」で唯一、天守内に神社(長壁神社)と厠がある。[wikipedia]"),
        Castle(id: 7, name: "松江城", place: "島根", nTreasure: true, builtBy: ["堀尾吉晴"], builtYr: 1611, rmks: "複合式望楼型5重6階の天守で、内部に井戸がある唯一の現存天守。外装は黒い下見板張りで、最上階には内廻縁と高欄を有し、鯱は木造銅板張である。2階に付けられた石落としなどの装備の点でも極めて実戦的な造りであり、漆黒の武骨荘重な意匠となっている。[wikipedia]"),
        Castle(id: 8, name: "備中松山城", place: "岡山", nTreasure: false, builtBy: ["秋庭重信"], builtYr: 1240, rmks: "渡櫓は失ってはいるが、複合式層塔型2重2階の天守で、現存天守の中では最も規模が小さい(高さ約11メートル)。現存建築の残る山城の遺構としては、備中松山城の例のみである。天守1階に囲炉裏が現存し、外観は、唐破風出窓や最上階の出格子の窓などにより重厚な意匠を醸し出している。[wikipedia]"),
        Castle(id: 9, name: "丸亀城", place: "香川", nTreasure: false, builtBy: ["奈良元安"], builtYr: 1597, rmks: "独立式層塔型3重3階の天守で、高さは約14.5メートルと弘前城天守(高さ約14.4メートル)の次に低い三重天守であるが、総高66メートルある総石垣の城の頂上に建てられている。一国一城令により廃城になったが、1660年(万治3年)に「御三階櫓」として建造された。最上重の屋根は平側面(南北面)に入母屋屋根の妻側を向け2重目の北面に向唐破風を一つ付けた外観は建物を大きく見せるためと見た目を重視したためである。[wikipedia]"),
        Castle(id: 10, name: "伊予松山城", place: "愛媛", nTreasure: false, builtBy: ["加藤嘉明"], builtYr: 1602, rmks: "層塔型3重3階地下1階の大天守と2重の小天守1基、2重櫓2基を多聞櫓で連結した連立式の天守で、平山城の比高において最も高い位置にある現存天守(標高約160メートル)である。天守丸の上に築かれた構造の天守は、黒船来航の翌年、将軍徳川家とゆかりのある松平家により復興されたもので、「現存12天守」で唯一、築城主として「葵の御紋」が付されており、また日本では最も新しい日本式城郭建築の天守である。1重・2重を下見板張り、3重目は白漆喰の塗られた外壁に飾りの外廻縁と高欄が付けられている。大天守各階は天井が張られ「床の間」が設けられている。また、「登り石垣(竪石垣)」や登城のための「城山索道」がある。また、愛媛県は現存天守が複数(2ヶ所)ある唯一の都道府県である。[wikipedia]"),
        Castle(id: 11, name: "宇和島城", place: "愛媛", nTreasure: false, builtBy: ["橘遠保"], builtYr: 941, rmks: "独立式層塔型3重3階の天守で、日本最南・最西端にある現存天守。白漆喰の塗られた外壁や破風と屋根・青銅製の鯱などの調和がよく取れた意匠である。また、「現存12天守」の中で、唯一、城内に障子建具が残っている。大名庭園である「天赦園」も現存している。[wikipedia]"),
        Castle(id: 12, name: "高知城", place: "高知", nTreasure: false, builtBy: ["山内一豊"], builtYr: 1603, rmks: "独立式望楼型4重6階の天守で、天守台がなく本丸御殿(現存)に入口がある現存天守。1747年(延久4年)に再建されたものであるが白漆喰で塗られた外壁に、2重目の大入母屋や千鳥破風、軒唐破風、実用的な外廻縁と擬宝珠高欄や大きな引戸が付けられた古式で開放的な意匠である。最上重屋根大棟上と2重目大入母屋屋根の大棟上には青銅製の鯱があげられている。[wikipedia]")
    ]
}

全部の情報がとれるか確認してみます。
ForEach はString配列

                        .navigationDestination(for: Castle.self) { castle in
                            VStack {
                                Text(castle.name)
                                Text(castle.place)
                                Text(castle.nTreasure ? "国宝" : "重要文化財")
                                ForEach(0..<castle.builtBy.count, id: \.self) { i in
                                    Text(castle.builtBy[i])
                                }
                                Text("\(String(castle.builtYr))年")
                                Text(castle.rmks)
                            }
                        }

DetailView

この遷移先のview を別view にします。
まず Viewプロトコルに適合したDetailView をつくります。
bodyの中に上でつくったVStack をそのままコピペします。

struct DetailView: View {
    var body: some View {
        VStack {
            Text(castle.name)
            Text(castle.place)
            Text(castle.nTreasure ? "国宝" : "重要文化財")
            ForEach(0..<castle.builtBy.count, id: \.self) { i in
                Text(castle.builtBy[i])
            }
            Text("\(String(castle.builtYr))年")
            Text(castle.rmks)
        }
    }
}
                       x Cannot find 'castle' in scope

エラーがでて castle プロパティが必要ということがわかるので置きます。
var castle: Castle とします。

struct DetailView: View {
    var castle: Castle

    var body: some View {
        VStack {
            Text(castle.name)
            Text(castle.place)
            Text(castle.nTreasure ? "国宝" : "重要文化財")
            ForEach(0..<castle.builtBy.count, id: \.self) { i in
                Text(castle.builtBy[i])
            }
            Text("\(String(castle.builtYr))年")
            Text(castle.rmks)
        }
    }
}

.navigationDestination の行き先を DetailView() に変えます。

// ContentView 

                        .navigationDestination(for: Castle.self) { castle in
                            DetailView(castle: castle)
                        }

DetailView ファイルを作って今作ったものを移します。

Preview の内容を ContentView() に変えます。

#Preview {
    ContentView()
//    DetailView()
}

これで横にCanvas をおいてDetailView を調整することができます。


ととのいました〜。

struct DetailView: View {
    var castle: Castle
    
    var body: some View {
        ScrollView {
            VStack(spacing: 12) {
                Text(castle.name)
                    .font(.largeTitle.weight(.bold))
                    .padding(.bottom, 16)
                
                VStack(alignment: .leading) {
                    HStack {
                        Text("築城者: ")
                        ForEach(0..<castle.builtBy.count, id: \.self) { i in
                            Text(castle.builtBy[i])
                        }
                        Spacer()
                    }
                    .padding(.bottom, 8)
                    
                    HStack {
                        Text("築城: ")
                        Text("\(String(castle.builtYr))年")
                    }
                    .padding(.bottom, 8)
                    
                    Text(castle.place)
                        .padding(.bottom, 8)
                    Text(castle.nTreasure ? "国宝" : "重要文化財")
                        .padding(.bottom, 8)
                }
                .font(.headline)
                .padding(.leading, 40)
                
                Text(castle.rmks)
                    .lineSpacing(6)
                    .padding(.top, 12)
                    .padding(.horizontal, 40)
                
                Spacer()
            }
            .padding(.top, 24)
        }
    }
}

なかなかデザインを一発で決めるのは難しいです。
トライアル& エラーで少しづつ調整していきます。

なおこのファイルはNavigationStack の影響下にあります
この構造体がNavigationStackで囲まれたbody 内で使われるからです。 
通常であれば padding() が効くはずなのに上手くいかない場合があります
そのような時はVStack や HStack などのコンテナに入れるとpadding() を効かせることができます

少しごちゃごちゃしてしまいましたが、いろいろと試した結果こうなりました。
ZStack, VStack, HStack を組み合わせていろいろと試していただければと思います。


次はContentView のリストのデザインを整えます。

先ほどと同じように、Viewを分けます
ListRowView を作り、NavigationLink 内にある HStack をコピペします。

struct ListRowView: View {
    var body: some View {
        HStack {
            Text(castle.name)
                .padding(.leading, 20)
            Spacer()
            Text(castle.place)
                .padding(.trailing, 20)
        }
        .padding(.vertical, 10)
        .background(.gray.opacity(0.1))
        .clipShape(RoundedRectangle(cornerRadius: 10))
    }
}

ContentView内の NavigationLink の中にListRowView() をセットします。

                        NavigationLink(value: castle) {
                            ListRowView(castle: castle)
                        }
                        .navigationDestination(for: Castle.self) { castle in
                            DetailView(castle: castle)
                        }

ListRowView

ListRowView ファイルを作成して先ほどと同じようにデザインを進めます。

その前に画面のバックグラウンドカラーをContentViewでセットしておきます。
以前に行った ZStack { Color. ~ の方法ではなくて NavigationStack の内側のScrollView { } に.background(Color.~) で設定します。

//  ContentView 

            .toolbar {
                Button {
                    showNTreasures.toggle()
                } label: {
                    Text("国宝")
                        .font(.headline)
                        .foregroundStyle(.white)
                        .shadow(radius: 1.5)
                        .padding(.horizontal, 8)
                        .padding(.vertical, 4)
                        .background(showNTreasures ? Color.red.gradient : Color.gray.gradient)
                        .clipShape(Capsule())
                }
            }
            .background(Color.purple.opacity(0.1))
        }
    }
}

ListRowViewを調整します

struct ListRowView: View {
    var castle: Castle
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 10)
                .foregroundStyle(.white.opacity(0.5))
                .overlay {
                    RoundedRectangle(cornerRadius: 10)
                        .stroke(Color.gray, lineWidth: 1.0)
                        .shadow(radius: 1.0, x: 1.0, y: 1.0)
                }
            HStack {
                Text(castle.name)
                    .font(.title2.weight(.bold))
                    .padding(.leading, 20)
                    .padding(.trailing, 8)
                VStack(alignment: .leading) {
                    HStack {
                        ForEach(0..<castle.builtBy.count, id: \.self) { i in
                            Text(castle.builtBy[i])
                        }
                    }
                    Text("\(String(castle.builtYr))年")
                }
                Spacer()
                Text(castle.place)
                    .padding(.trailing, 20)
            }
            .foregroundStyle(.primary)
            .padding(.trailing, 8)
            .padding(.vertical, 12)
        }
        .accentColor(.primary)
    }
}

リストひとつひとつの背景を整えるために、ZStack でバックに 丸四角 RoundedRectangle() をおいてそれを修飾することで見栄えを綺麗にしています。
テキストはメリハリを持たせて大きい文字で城名を、通常文字で築城者、築城年を表示しています。またそこをVStack で縦並びにしています。
このようなパターンは多く使われるものです。
左端に城の全景写真を丸抜きなどでもってくるのもパターンのひとつです。

全般にカラーにopacity() で透明度を高めることによってダークモードのときにメリハリを持たせるようにしています。
もしダークモードで暗くしたくない場合はそのように設定しておきます。
逆も同様です。 
.preferredColorScheme(.light)  または (.dark) を body 内につけます。

        .preferredColorScheme(.light)
// or
        .preferredColorScheme(.dark)

これで、このアプリではいつも同じモードとなりますが、Canvas上では変わってしまいますので、確認の際はシミュレーターまたは実機で行います
Canvas ではオリエンテーション(端末向き)やこのダークモード設定などを止めることができないようなので注意が必要です。
なお今回は特にこれらの設定は行いません。


ひとまずこれで良い形ななったのではないでしょうか?


ForEach

最後に念の為シミュレーターでも確認してみましょう。

そうしたらなんとエラーが沢山でてきました。
どうもnavigationDestination が関係しているようです。

ただ操作しても特に問題ないようです。

私はこのまま知らんふりして終わってしまおうかと、何度も思いました。
ここで MacBook Pro の画面を閉じてしまえば、それで終了する。
使用にはなんの問題もないではないか。
自分の端末で試してもとても滑らかな画面遷移を感じることができる。
俺は何を躊躇しているんだ。
あとは、そこにある画面に手を伸ばすだけだ  〜 〜 〜



あ〜 失礼しました。
これは、ForEach() に関連して発生するエラーだと思われます。
.navigationDestination を説明するときに、Lazy なList やLazyVStack などの中に入れないよう注意がありましたが、この ForEach() もそれに類するものと考えておくべきかもしれません。
ForEach()は一瞬で作業を終了させるものの、現実には1枚1枚ページをめくるような処理をしています。そのページをめくっている間にLazy な人が「よろしく〜」といって入ってくると几帳面なForEach くんにとっては目障りなのです。


そこで .navigationDestination には ForEachくんの外にでてもらうことにしました。

                VStack {
                    ForEach(filteredCastles) { castle in
                        NavigationLink(value: castle) {
                            ListRowView(castle: castle)
                        }
                    }
                    .navigationDestination(for: Castle.self) { castle in
                        DetailView(castle: castle)
                    }
                    if showNTreasures {
                        Text("国宝5城")

そうしたらエラーは表示されなくなりました。
今度は大丈夫そうです。


今回はここまでとなります。
次回はコードの整理を少し行なってみたいと思います。
ですので、このProject は次回までとっておいてください。
または、最後にコードを載せておきますので、次回それをコピーしてください。



まとめ

いかがでしたでしょうか?
画面がアプリらしくなってくると楽しさもひとしおですね。
最初は城の設計図として名前と場所があって、そこから全国の城として3つをつくって、、、というところからスタートしましたが、そこからよく成長してくれました。
では次回もこのお城を扱いますのでよろしくお願いします。

次回第22回内容
  次回は  オブジェクトとリスト 4. です。
          よろしくお願いします。


今回のこれまでのコード全文

import SwiftUI

struct ContentView: View {
    let castles = AllCastles()
    @State private var searchText: String = ""
    var filteredCastles: [Castle] {
        let trimmedText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmedText.isEmpty {
            return nTreasureFiltered
        } else {
            var filtered = [Castle]()
            
            let filterName = nTreasureFiltered.filter { $0.name.contains(searchText) }
            let filterPlace = nTreasureFiltered.filter { $0.place.contains(searchText) }
            
            filtered.append(contentsOf: filterName)
            filtered.append(contentsOf: filterPlace)
            
            return filtered
        }
    }
    
    @State private var showNTreasures: Bool = false
    var nTreasureFiltered: [Castle] {
        if showNTreasures {
            let filtered = castles.items.filter { $0.nTreasure }
            return filtered
        } else {
            return castles.items
        }
    }
    
    var body: some View {
        NavigationStack {
            ScrollView {
                VStack {
                    ForEach(filteredCastles) { castle in
                        NavigationLink(value: castle) {
                            ListRowView(castle: castle)
                        }
                    }
                    .navigationDestination(for: Castle.self) { castle in
                        DetailView(castle: castle)
                    }
                    if showNTreasures {
                        Text("国宝5城")
                            .frame(maxWidth: .infinity, alignment: .trailing)
                    } else {
                        Text("現存天守12城")
                            .frame(maxWidth: .infinity, alignment: .trailing)
                    }
                }
                .padding(.horizontal, 24)
                .frame(maxWidth: 800)
            }
            .navigationTitle("日本の名城")
            .searchable(text: $searchText)
            .toolbar {
                Button {
                    showNTreasures.toggle()
                } label: {
                    Text("国宝")
                        .font(.headline)
                        .foregroundStyle(.white)
                        .shadow(radius: 1.5)
                        .padding(.horizontal, 8)
                        .padding(.vertical, 4)
                        .background(showNTreasures ? Color.red.gradient : Color.gray.gradient)
                        .clipShape(Capsule())
                }
            }
            .background(Color.purple.opacity(0.1))
        }
    }
}

#Preview {
    ContentView()
}

struct Castle: Identifiable, Hashable {
    let id: Int
    let name:String
    let place: String
    let nTreasure: Bool
    let builtBy: [String]
    let builtYr: Int
    let rmks: String
}

struct AllCastles {
    let items = [
        Castle(id: 1, name: "弘前城", place: "青森", nTreasure: false, builtBy: ["津軽為信", "津軽信枚"], builtYr: 1611, rmks:  "独立式層塔型3重3階の天守で、現在の現存天守の中では日本最北かつ最東端の地にある。1627年(寛永4年)に落雷により天守を焼失した後、1810年(文化7年)に幕府にはばかって名目上櫓として3重3階層塔型構造で新築したもので、「御三階櫓」と名付けられた。城外側にあたる東・南面には切妻破風を2重に重ねて出窓や出張を設け、窓の代わりに矢狭間を用いた構えであるが、城内側にあたる西・北面には天守建築の特徴の一つである破風がなく、連続した窓があけられている。また、凍結に対応するために鯱や屋根は銅瓦(木型の上に銅板を貼り付けているもの)が用いられている。[wikipedia]"),
        Castle(id: 2, name: "松本城", place: "長野", nTreasure: true, builtBy: ["石川数正", "石川康長"], builtYr: 1593, rmks: "層塔型5重6階の大天守と3重4階の乾小天守、2重の辰巳附櫓と月見櫓を付属させた複合連結式の天守で、「現存12天守」の中では唯一平城の天守である。破風が少なく、黒塗りの下見板がめぐらされているため、漆黒で簡素な外観であるが、複合連結式であるため見る角度によって異なる印象の意匠を見ることができる。[wikipedia]"),
        Castle(id: 3, name: "丸岡城", place: "福井", nTreasure: false, builtBy: ["柴田勝豊"], builtYr: 1576, rmks: "独立式望楼型2重3階の天守で、最古の現存天守とする説もある。1948年(昭和23年)の福井地震により倒壊したが、元の古材を80パーセント近く使用して1955年(昭和30年)に再建された。飾り外廻縁と高欄を有し、「現存12天守」の中では採光がよく室内が明るい点や、凍結で割れてしまう粘土瓦の代わりに石瓦が葺かれている(石の鯱も展示)ことなどの特徴がある。[wikipedia]"),
        Castle(id: 4, name: "犬山城", place: "愛知", nTreasure: true, builtBy: ["織田広近"] , builtYr: 1469, rmks: "複合式望楼型3重4階地下2階の天守で、大入母屋屋根の建築の上に外廻縁側を突出させた小規模な望楼を上げた形状は丸岡城天守と同様である。この天守は最上階に実用的な外廻縁と高欄が付けられ、華頭窓も付けられているが、実際は窓ではなく装飾である。小屋裏となる3階にも唐破風出窓を設けるなどの採光が考慮されている。[wikipedia]"),
        Castle(id: 5, name: "彦根城", place: "滋賀", nTreasure: true, builtBy: ["井伊直継"], builtYr: 1622, rmks: "複合式望楼型3重3階地下1階の天守で、幕府の普請(天下普請)による。飾り外廻縁と高欄を持ち、切妻破風、入母屋破風、千鳥破風、唐破風が組み合った、複雑な構造美と輪郭の荘厳な景観の意匠となっている。また、文禄・慶長の役の際に朝鮮半島に造られた倭城にも見られる「登り石垣(竪石垣)」や大名庭園「玄宮園」も現存する。[wikipedia]"),
        Castle(id: 6, name: "姫路城", place: "兵庫", nTreasure: true, builtBy: ["赤松貞範"], builtYr: 1346, rmks: "望楼型5重6階地下1階の大天守と3重の小天守3基を2重の多聞櫓で連結させた連立式の天守で、日本国内最大の現存天守である。白漆喰で塗られた白亜の外壁と屋根や破風の構成美の上、見る方向により異なった趣となる連立式であり、また桃山後期から江戸初期当時の作事(建築)の技を現代に伝える代表的な城郭と言われている。また、「現存12天守」で唯一、天守内に神社(長壁神社)と厠がある。[wikipedia]"),
        Castle(id: 7, name: "松江城", place: "島根", nTreasure: true, builtBy: ["堀尾吉晴"], builtYr: 1611, rmks: "複合式望楼型5重6階の天守で、内部に井戸がある唯一の現存天守。外装は黒い下見板張りで、最上階には内廻縁と高欄を有し、鯱は木造銅板張である。2階に付けられた石落としなどの装備の点でも極めて実戦的な造りであり、漆黒の武骨荘重な意匠となっている。[wikipedia]"),
        Castle(id: 8, name: "備中松山城", place: "岡山", nTreasure: false, builtBy: ["秋庭重信"], builtYr: 1240, rmks: "渡櫓は失ってはいるが、複合式層塔型2重2階の天守で、現存天守の中では最も規模が小さい(高さ約11メートル)。現存建築の残る山城の遺構としては、備中松山城の例のみである。天守1階に囲炉裏が現存し、外観は、唐破風出窓や最上階の出格子の窓などにより重厚な意匠を醸し出している。[wikipedia]"),
        Castle(id: 9, name: "丸亀城", place: "香川", nTreasure: false, builtBy: ["奈良元安"], builtYr: 1597, rmks: "独立式層塔型3重3階の天守で、高さは約14.5メートルと弘前城天守(高さ約14.4メートル)の次に低い三重天守であるが、総高66メートルある総石垣の城の頂上に建てられている。一国一城令により廃城になったが、1660年(万治3年)に「御三階櫓」として建造された。最上重の屋根は平側面(南北面)に入母屋屋根の妻側を向け2重目の北面に向唐破風を一つ付けた外観は建物を大きく見せるためと見た目を重視したためである。[wikipedia]"),
        Castle(id: 10, name: "伊予松山城", place: "愛媛", nTreasure: false, builtBy: ["加藤嘉明"], builtYr: 1602, rmks: "層塔型3重3階地下1階の大天守と2重の小天守1基、2重櫓2基を多聞櫓で連結した連立式の天守で、平山城の比高において最も高い位置にある現存天守(標高約160メートル)である。天守丸の上に築かれた構造の天守は、黒船来航の翌年、将軍徳川家とゆかりのある松平家により復興されたもので、「現存12天守」で唯一、築城主として「葵の御紋」が付されており、また日本では最も新しい日本式城郭建築の天守である。1重・2重を下見板張り、3重目は白漆喰の塗られた外壁に飾りの外廻縁と高欄が付けられている。大天守各階は天井が張られ「床の間」が設けられている。また、「登り石垣(竪石垣)」や登城のための「城山索道」がある。また、愛媛県は現存天守が複数(2ヶ所)ある唯一の都道府県である。[wikipedia]"),
        Castle(id: 11, name: "宇和島城", place: "愛媛", nTreasure: false, builtBy: ["橘遠保"], builtYr: 941, rmks: "独立式層塔型3重3階の天守で、日本最南・最西端にある現存天守。白漆喰の塗られた外壁や破風と屋根・青銅製の鯱などの調和がよく取れた意匠である。また、「現存12天守」の中で、唯一、城内に障子建具が残っている。大名庭園である「天赦園」も現存している。[wikipedia]"),
        Castle(id: 12, name: "高知城", place: "高知", nTreasure: false, builtBy: ["山内一豊"], builtYr: 1603, rmks: "独立式望楼型4重6階の天守で、天守台がなく本丸御殿(現存)に入口がある現存天守。1747年(延久4年)に再建されたものであるが白漆喰で塗られた外壁に、2重目の大入母屋や千鳥破風、軒唐破風、実用的な外廻縁と擬宝珠高欄や大きな引戸が付けられた古式で開放的な意匠である。最上重屋根大棟上と2重目大入母屋屋根の大棟上には青銅製の鯱があげられている。[wikipedia]")
    ]
}
import SwiftUI

struct DetailView: View {
    var castle: Castle
    
    var body: some View {
        ScrollView {
            VStack(spacing: 12) {
                Text(castle.name)
                    .font(.largeTitle.weight(.bold))
                    .padding(.bottom, 16)
                
                VStack(alignment: .leading) {
                    HStack {
                        Text("築城者: ")
                        ForEach(0..<castle.builtBy.count, id: \.self) { i in
                            Text(castle.builtBy[i])
                        }
                        Spacer()
                    }
                    .padding(.bottom, 8)
                    
                    HStack {
                        Text("築城: ")
                        Text("\(String(castle.builtYr))年")
                    }
                    .padding(.bottom, 8)
                                        
                    HStack {
                        VStack(alignment: .leading) {
                            Text(castle.place)
                                .padding(.bottom, 8)
                            Text(castle.nTreasure ? "国宝" : "重要文化財")
                                .padding(.bottom, 8)
                        }
                        Spacer()
                        
                        ZStack {
                            RoundedRectangle(cornerRadius: 8)
                                .frame(width: 80, height: 80)
                                .foregroundStyle(.gray.opacity(0.25))
                            Text("紋章")
                                .font(.title2.weight(.bold))
                                .foregroundStyle(.white)
                                .shadow(radius: 2.0)
                        }
                        .padding(.trailing, 40)
                        .padding(.bottom, 8)

                    }
                }
                .font(.headline)
                .padding(.leading, 40)
                
                Text(castle.rmks)
                    .lineSpacing(6)
                    .padding(.top, 12)
                    .padding(.horizontal, 40)
                
                Spacer()
            }
            .padding(.top, 24)
        }
    }
}

#Preview {
    ContentView()
    //    DetailView()
}
import SwiftUI

struct ListRowView: View {
    var castle: Castle
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 10)
                .foregroundStyle(.white.opacity(0.5))
                .overlay {
                    RoundedRectangle(cornerRadius: 10)
                        .stroke(Color.gray, lineWidth: 1.0)
                        .shadow(radius: 1.0, x: 1.0, y: 1.0)
                }
            HStack {
                Text(castle.name)
                    .font(.title2.weight(.bold))
                    .padding(.leading, 20)
                    .padding(.trailing, 8)
                VStack(alignment: .leading) {
                    HStack {
                        ForEach(0..<castle.builtBy.count, id: \.self) { i in
                            Text(castle.builtBy[i])
                        }
                    }
                    Text("\(String(castle.builtYr))年")
                }
                Spacer()
                Text(castle.place)
                    .padding(.trailing, 20)
            }
            .foregroundStyle(.primary)
            .padding(.trailing, 8)
            .padding(.vertical, 12)
        }
        .accentColor(.primary)
    }
}

#Preview {
    ContentView()
    //    ListRowView()
}
import SwiftUI

@main
struct ObjectPracticeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

以上です


いいなと思ったら応援しよう!

MasaOno
サポートいただければ、さらに活動を広げることができます!