スマホアプリで一人回しがしたい(ついでにSwiftUIお勉強)その2

導入 スマホで一人回しがしたい

どうも、湊です。

私はTCGでよく一人回しというのをします。*1

ただこの一人回し、自宅などカードを広げることが許されるスペースでしかできません(当然)。

どこでもやりたくない...?
スマホでやりたくない...?

と思ったのでアプリを作ることにしました。
趣味のツール作成と仕事の勉強(SwiftUI)ができて一石二鳥です。

前回まで

前回↓はキャラクターを配置するViewをSwiftUIで作りました。
スマホアプリで一人回しがしたい(ついでにSwiftUIお勉強)① - minato_blog

今回は盤面全体を作っていくことにします。

画面イメージ(ざっくり)

スマホの画面に収める必要があるため、実際の盤面からは少々レイアウトを変える必要がありそうです。
こんな感じです。

f:id:minato_lotus:20201114153142p:plain
画面イメージ

ピンク色のマスがAFで青色はDFです。
山札や置き場は、ぱっと見でわかればいいのが枚数だけなのでまずはそれだけ表示することにします。
(山札などの中身が見れる画面は別で作ります)

AF, DF

前回作ったフィールド一つあたりのViewを6つ並べるだけでよさそうです。

これはVStackとHStackを用いることで簡単に実現できます。(Viewの表示用データを格納するstructも一緒に記載しておきます)

struct Fields {
    var leftAF = Field()
    var centerAF = Field()
    var rightAF = Field()
    var leftDF = Field()
    var centerDF = Field()
    var rightDF = Field()
}

struct FieldsView: View {
    var fields: Fields
    
    var body: some View {
        GeometryReader { geometry in
            let fieldWidth = min(geometry.size.width, geometry.size.height) / 3.2
            
            VStack {
                HStack {
                    FieldView(field: fields.leftAF)
                        .frame(width: fieldWidth, height: fieldWidth)
                    FieldView(field: fields.centerAF)
                        .frame(width: fieldWidth, height: fieldWidth)
                    FieldView(field: fields.rightAF)
                        .frame(width: fieldWidth, height: fieldWidth)
                }
                HStack {
                    FieldView(field: fields.leftDF)
                        .frame(width: fieldWidth, height: fieldWidth)
                    FieldView(field: fields.centerDF)
                        .frame(width: fieldWidth, height: fieldWidth)
                    FieldView(field: fields.rightDF)
                        .frame(width: fieldWidth, height: fieldWidth)
                }
            }
            .frame(width: geometry.size.width)
        }
    }
}

これをPreviewで見てみるとこんな感じになります。

f:id:minato_lotus:20201114171325p:plain
Preview

山札・ゴミ箱・その他置き場の情報

とりあえずその領域の名前とカード枚数だけわかればよさそうなので、Textで配置しました。

コード。ここを押して展開。

struct DomainInfoView: View {
    var onePlayerDomain: PlayerDomain
    var otherPlayerDomain: PlayerDomain
    
    init(one oneDomain: PlayerDomain, other otherDomain: PlayerDomain) {
        self.onePlayerDomain = oneDomain
        self.otherPlayerDomain = otherDomain
    }
    
    var body: some View {
        GeometryReader { geometry in
            VStack {
                HStack {
                    Spacer()
                    HStack {
                        VStack {
                            HStack {
                                VStack {
                                    Text("山札")
                                    Text("\(otherPlayerDomain.deck.cards.count)枚")
                                }
                                .padding(.leading, /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/)
                                VStack {
                                    Text("ゴミ箱")
                                    Text("\(otherPlayerDomain.trashBox.cards.count)枚")
                                }
                                Spacer()
                            }
                            .font(.system(size: 12))
                            HStack {
                                ForEach(otherPlayerDomain.somePlaces, id: \.name) { place in
                                    VStack {
                                        Text(place.name)
                                        Text("\(place.cards.count)枚")
                                    }
                                }
                            }
                            .font(.system(size: 10))
                            .padding(.bottom, 8)
                        }
                    }
                    .frame(width: geometry.size.width * 0.45)
                    .overlay(
                        Rectangle()
                            .frame(width: geometry.size.width * 0.45, height: 1),
                        alignment: .bottom
                    )
                    .overlay(
                        Rectangle()
                            .frame(width: 1),
                        alignment: .leading
                    )
                    .overlay(
                        Rectangle()
                            .frame(width: 1),
                        alignment: .trailing
                    )
                    Spacer()
                    HStack {
                        VStack {
                            HStack {
                                Spacer()
                                VStack {
                                    Text("山札")
                                    Text("\(onePlayerDomain.deck.cards.count)枚")
                                }
                                VStack {
                                    Text("ゴミ箱")
                                    Text("\(onePlayerDomain.trashBox.cards.count)枚")
                                }
                                .padding(.trailing, /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/)
                            }
                            .font(.system(size: 12))
                            .padding(.top, 8)
                            HStack {
                                ForEach(onePlayerDomain.somePlaces, id: \.name) { place in
                                    VStack {
                                        Text(place.name)
                                        Text("\(place.cards.count)枚")
                                    }
                                }
                            }
                            .font(.system(size: 10))
                        }
                    }
                    .frame(width: geometry.size.width * 0.45)
                    .overlay(
                        Rectangle()
                            .frame(width: geometry.size.width * 0.45, height: 1),
                        alignment: .top
                    )
                    .overlay(
                        Rectangle()
                            .frame(width: 1),
                        alignment: .leading
                    )
                    .overlay(
                        Rectangle()
                            .frame(width: 1),
                        alignment: .trailing
                    )
                    Spacer()
                }
                .frame(width: geometry.size.width, height: geometry.size.height)
            }
        }
    }
}

やってることは単純なのですが、枠をつけるためのコードで行数が嵩んでしまいました。。
表示はこんな感じになります。

f:id:minato_lotus:20201114203659p:plain
山・ゴミ箱・その他置き場

手札

カード情報から画像を取得して、HStackとForEachで並べます。

struct Hand {
    var cards: [Card]()
}

struct HandView: View {
    var hand: Hand
    
    var body: some View {
        HStack {
            ForEach(hand.cards.indices) { index in
                Image(hand.cards[index].imageName)
                    .resizable()
                    .scaledToFit()
            }
        }
    }
}

盤面全体

それではここまでで作ってきたViewで画面を組み立てましょう

f:id:minato_lotus:20201114205037p:plain
盤面構成図

コードは上の図の通り並べるだけです。

struct BoardView: View {
    var board: Board
    
    var body: some View {
        GeometryReader { geometry in
            VStack {
                HandView(hand: board.otherDomain.hand)
                    .frame(width: geometry.size.width, height: geometry.size.width / 8)
                FieldsView(fields: board.otherDomain.fields)
                    .rotationEffect(Angle(degrees: 180))
                DomainInfoView(one: board.oneDomain, other: board.otherDomain)
                    .frame(width: geometry.size.width, height: geometry.size.width / 6, alignment: .center)
                FieldsView(fields: board.oneDomain.fields)
                HandView(hand: board.oneDomain.hand)
                    .frame(width: geometry.size.width, height: geometry.size.width / 8)
            }
        }
    }
}

すごい簡単ですね。ここまででこんな感じの表示になります。

f:id:minato_lotus:20201115014556p:plain
Preview

まだちょっと味気ないですが、必要なものは表示できたのではないでしょうか。

その2はここまで。
その3の記事では実際に表示しているこれらのカードを操作する部分をつくっていきます。


各種カード画像はLycee公式サイトから拝借しました。

LYCEE OVERTURE TRADING CARD GAME || リセ オーバーチュア トレーディングカードゲーム

*1:一人回し:一人で2つのデッキを使って練習すること