Sponsor Link
環境&対象
- macOS Big Sur 11.2
- Xcode 12.4
- iOS 14.4
概要
前回、サイコロのビューを作成したので、再利用しやすいようにコンポーネント化してみます。
前回の記事はこちら。
[SwiftUI] サイコロを振ってみる (SwiftUI アニメーションの練習)
サイコロ表示とボタンをセットでビュー化
DiceView というビューを作って、親ビューから dice 変数を binding で受け取るようにします。
以下が、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 からの発行でサイコロを振ることにします。
//
// 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
}
}
- Publisher として、PassthroughSubject を使用します
- DiceView の初期化時に @Binding 変数と Publisher を渡します
- 渡された Publisher から発行されたときに、roll 関数を呼ぶようにします
こうすることで、親ビューからのリクエストで roll 関数を実行することができます。
ここまでの変更は、これまでのアプリでの見た目的には、同じですが、例えば、複数のサイコロを並べて同時に振るというアプリを作るときにも再利用できるビューとなっています。
3つのサイコロを振るアプリです。
以下のコードで実現しています。(DiceView は、再利用しているため省略しています。)
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 経由で受けることでも、可能です。
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link