[SwiftUI] Combine をつかって サイコロを振ってみる

     
⌛️ 2 min.
作ったサイコロビューを再利用しやすいようにコンポーネント化します

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.2
  • Xcode 12.4
  • iOS 14.4

概要

前回、サイコロのビューを作成したので、再利用しやすいようにコンポーネント化してみます。

前回の記事はこちら。
SwiftUI [SwiftUI] サイコロを振ってみる (SwiftUI アニメーションの練習)

サイコロ表示とボタンをセットでビュー化

DiceView というビューを作って、親ビューから dice 変数を binding で受け取るようにします。

以下が、DiceView として再利用できるようにしたビューです。

DiceView


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/02/03
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @State private var dice:Int = Int.random(in:1...6)
    var body: some View {
        VStack {
            // (1) サイコロを振った結果を @State/@Binding 変数経由で受け取る
            DiceView(dice: $dice)
        }
        .padding()
    }
}

struct DiceView: View {
    @Binding var dice:Int
    @State private var animate = false

    var body: some View {
        Image("\(dice)")
            .resizable()
            .scaledToFit()
            .rotation3DEffect(
                Angle.degrees(animate ? 360 * 20 : 0),
                axis: (x:1, y:1, z:1))
        Button(action: {
            withAnimation(Animation.default.repeatForever()) {
                animate.toggle()
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                withAnimation {
                    self.dice = Int.random(in: 1...6)
                    animate.toggle()
                }
            }
        }, label: {
            Text("Roll")
        })
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

考察

サイコロを振るビューとしては良いのですが、複数のサイコロを振る等の機能を実現したいときに、サイコロを振るボタンまで含まれたビューは使いにくいです。

ということで、外部からサイコロを振ることができるようにしてみます。

Publisher を使って、ボタンを外部化

さまざまな方法がありますが、Combine の練習という意味も含め、Publisher を使って、親ビューから子ビューへイベント発行してみます。

使用する Publisher

方針としては、親ビューが Publisher を持っていて、子ビューは、Publisher を subscribe するようにします。
そして、サイコロを降りたいときに、親ビューが Publisher でイベントを発行することで、子ビューは、サイコロを振ります。
振った結果は、@Binding 経由で親ビューが取得できるハズです。

単に「サイコロを振って欲しい」というリクエストを出したいだけで詳細の情報は不要ですので、Publisher の型としては、<Void,Never> で十分です。

自分の好きなタイミングで発行したいので、PassthroughSubject を使います。

DiceView はその Publisher からの発行でサイコロを振ることにします。

DiceView Combine 改良版


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/02/03
//  © 2021  SmallDeskSoftware
//

import SwiftUI
import Combine

struct ContentView: View {
    @State private var dice:Int = Int.random(in: 1...6)
    // (1)
    var requester = PassthroughSubject<Void,Never>()
    var body: some View {
        VStack {
            HStack {
                // (2)
                DiceView($dice, requester.eraseToAnyPublisher())
            }
            Text("\(dice)")
            Button(action: {
                self.requester.send()
            }, label: {
                Text("Roll")
            })
        }
        .padding()
    }
}

struct DiceView: View {
    @Binding var dice:Int
    let publisher:AnyPublisher<Void,Never>
    @State private var animate = false
    
    init(_ dice:Binding<Int>, _ requester:AnyPublisher<Void,Never>) {
        self._dice = dice
        self.publisher = requester
    }
    
    var body: some View {
        VStack {
            Image("\(dice)")
                .resizable()
                .scaledToFit()
                .rotation3DEffect(
                    Angle.degrees(animate ? 360 * 20 : 0),
                    axis: (x:1, y:1, z:1))
        }
        // (3)
        .onReceive(publisher) { () in
            self.roll()
        }
    }
    public func roll() {
        withAnimation(Animation.default.repeatForever()) {
            animate.toggle()
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            withAnimation {
                self.dice = Int.random(in: 1...6)
                animate.toggle()
            }
        }
        return
    }
}
コード解説
  1. Publisher として、PassthroughSubject を使用します
  2. DiceView の初期化時に @Binding 変数と Publisher を渡します
  3. 渡された Publisher から発行されたときに、roll 関数を呼ぶようにします

こうすることで、親ビューからのリクエストで roll 関数を実行することができます。

ここまでの変更は、これまでのアプリでの見た目的には、同じですが、例えば、複数のサイコロを並べて同時に振るというアプリを作るときにも再利用できるビューとなっています。

3つのサイコロを振るアプリです。

以下のコードで実現しています。(DiceView は、再利用しているため省略しています。)

DiceView を使って3つのサイコロを振るアプリ


struct ContentView: View {
    @State private var dice1:Int = Int.random(in: 1...6)
    @State private var dice2:Int = Int.random(in: 1...6)
    @State private var dice3:Int = Int.random(in: 1...6)
    var requester = PassthroughSubject()
    var body: some View {
        VStack {
            HStack {
                DiceView($dice1, requester.eraseToAnyPublisher())
                DiceView($dice2, requester.eraseToAnyPublisher())
                DiceView($dice3, requester.eraseToAnyPublisher())
            }
            Text("\(dice1) - \(dice2) - \(dice3)")
            Button(action: {
                self.requester.send()
            }, label: {
                Text("Roll")
            })
        }
        .padding()
    }
}

下位のビューから @State/@Binding 経由で情報を受け取ることはよくありますが、下位のビューの関数を呼ぶのは、意外と難しいことに気づきました。

# NotificationCenter の Notification 経由で受けることでも、可能です。

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です