この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
概要
大阪オフィスの山田です。今回、SwiftUIでFloating Buttonを趣味アプリで実装してみたので、やり方メモを公開します。
環境
- Xcode: 11.5
- macOS: 10.15.4
Floating Buttonを作る
このようなボタンを実装します。
以下の実装でFloating Buttonの外観を作ります。
struct FloatingButton: View {
var body: some View {
VStack { // --- 1
Spacer()
HStack { // --- 2
Spacer()
Button(action: {
print("Tapped!!") // --- 3
}, label: {
Image(systemName: "pencil")
.foregroundColor(.white)
.font(.system(size: 24)) // --- 4
})
.frame(width: 60, height: 60)
.background(Color.orange)
.cornerRadius(30.0)
.shadow(color: .gray, radius: 3, x: 3, y: 3)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 16.0, trailing: 16.0)) // --- 5
}
}
}
}
プログラムを解説します。
- 1, 2: ボタンが右下に配置されるよう、
VStack
とHStack
とSpacer
を使用 - 3: ボタンをタップした時のアクション
- 現時点ではまだ、文字を出力するだけの処理。ここは後ほど実装します。
- 4: ボタンのラベル部分を作成
- 今回はシステムで用意されている画像を使って、色を白色にしています。そのままでは少し小さいので
font
メソッドを使って、大きくしています。
- 今回はシステムで用意されている画像を使って、色を白色にしています。そのままでは少し小さいので
- 5: ボタン全体のデザイン
- 大きさは固定値指定、
cornerRadius
も固定値指定にして丸くしています。background
で背景色を設定しています。padding
で、ボタンの下と右にスペースを確保しています。このメソッドを省くと、画面右下にぴったりくっつきます。そして、最後にshadow
で影をつけています。
- 大きさは固定値指定、
これでFloating Buttonの外観ができたので、実際に、浮いたボタンになっているか確認します。
Floating Buttonをパーツとして表示する
実際にFloating Buttonを浮いたように表示するにはZStack
を使います。これを使うことでViewを重ね合わせたような外観を実装することができます。
struct InputView: View {
var body: some View {
ZStack {
VStack {
Spacer()
HStack() {
Spacer()
Text("Yattane!")
.font(.title)
}
}
FloatingButton()
}
}
}
実装したInputView
を表示すると、以下の画像のようになります。Text
の上にボタンが被さっているのがわかります。
TextFieldでキーボードが表示された時に隠れないようにする
この実装をするには、キーボードが表示された時にキーボードの高さを取得、通知するクラスが必要です。SwiftUIでキーボードで文字が隠れないように処理をいれる: Qiitaや、KeyboardObserving: GitHubの実装が参考になります。Notification Centerを使ってキーボードの高さを取得します。今回はキーボード領域が被った分だけずらす、のような複雑なことはせず、単にキーボードが表示されたら高さを、非表示になれば0を設定するようにしています。
class KeyboardService: ObservableObject {
/// キーボードの高さを伝えるプロパティ
@Published var keyboardHeight: CGFloat = 0.0
let defaultNotification = NotificationCenter.default
/// キーボードの監視開始
func start() {
defaultNotification.addObserver(
self,
selector: #selector(self.keyboardWillShow(_:)),
name: UIResponder.keyboardWillShowNotification,
object: nil)
defaultNotification.addObserver(
self,
selector: #selector(self.keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
/// キーボードの監視終了
func end() {
NotificationCenter
.default
.removeObserver(
self,
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter
.default
.removeObserver(
self,
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
/// キーボードが表示されたら、高さをkeyboardHeighに設定する
@objc func keyboardWillShow(_ notification: Notification) {
guard let rect = (notification
.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return }
self.keyboardHeight = rect.size.height
}
/// キーボードが表示されたら、高さを0に設定する
@objc func keyboardWillHide(_ notification: Notification) {
self.keyboardHeight = 0
}
}
これで、キーボードの高さを取得するクラスが実装できました。次にViewに繋ぎこみます。
struct InputView: View {
@State var inputText = ""
@ObservedObject var keyboard = KeyboardService() // --- 1
var body: some View {
ZStack {
VStack {
TextField(
"入力してね",
text: $inputText,
onEditingChanged: { editing in
}).padding() // --- 5
}
FloatingButton(bottomPadding: self.keyboard.keyboardHeight) // --- 2
}.onAppear {
self.keyboard.start() // --- 3
}.onDisappear {
self.keyboard.end() // --- 4
}
}
}
- 1:
ObservedObject
としてKeyboardService
を宣言 - 2:
KeyboardService
を使ってFloatingButton
にキーボードの高さを伝えるFloatingButton
クラスは後ほど修正します。
- 3, 4: Appear時、DisAppear時にそれぞれ、キーボードの監視を開始、終了
- 5: 入力用の
TextField
を用意
続いて、FloatingButton
の実装に進みます。
struct FloatingButton: View {
var bottomPadding: CGFloat = 0 // --- 1
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
print("Tapped!!")
}, label: {
Image(systemName: "pencil")
.foregroundColor(.white)
.font(.system(size: 24))
})
.frame(width: 60, height: 60)
.background(Color.orange)
.cornerRadius(30.0)
.shadow(color: .gray, radius: 3, x: 3, y: 3)
.padding(EdgeInsets(top: 0, leading: 0, bottom: bottomPadding + 16.0, trailing: 16.0)) // --- 2
.animation(.easeOut)
}
}
}
}
- 1: ボタン下側のpaddingを保持する変数を宣言
- 2: paddingの適用
キーボードが表示された時、高さが伝わり、paddingの値を変更する、という仕組みになっています。
実際に動かすと以下のようになります。
ボタンタップ時の動きを生成時に指定する
ボタンタップ時に入力された文字を画面に表示するようにします。それでは実装していきます。まず、ボタンタップで入力された画面を表示します。
struct InputView: View {
@State var inputText = ""
@State var outputText = "" // --- 1
@ObservedObject var keyboard = KeyboardService()
var body: some View {
ZStack {
VStack {
Text(outputText) // --- 2
TextField(
"入力してね",
text: $inputText,
onEditingChanged: { editing in
}).padding()
}
FloatingButton(
bottomPadding: self.keyboard.keyboardHeight,
tappedHandler: self.showText // --- 3
)
}.onAppear {
self.keyboard.start()
}.onDisappear {
self.keyboard.end()
}
}
// --- 4
func showText() {
outputText = inputText
}
}
struct FloatingButton: View {
var bottomPadding: CGFloat = 0
var tappedHandler: (() -> Void)? = nil // --- 5
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
self.tappedHandler?() // --- 6
}, label: {
Image(systemName: "pencil")
.foregroundColor(.white)
.font(.system(size: 24))
})
.frame(width: 60, height: 60)
.background(Color.orange)
.cornerRadius(30.0)
.shadow(color: .gray, radius: 3, x: 3, y: 3)
.padding(EdgeInsets(top: 0, leading: 0, bottom: bottomPadding + 16.0, trailing: 16.0))
.animation(.easeOut)
}
}
}
}
まず、InputView
の修正です。
- 1: 表示する文字列を格納する変数を用意
- 2: 変数outputTextをTextで表示
- 3: ボタンをタップした時に動作させる内容を指定
- ここでは4にてメソッドを定義して、それを呼び出すようにしています。
続いてFloatingButton
です。
- 5: タップ時に動作させるハンドラーを変数として定義
- 6: タップ時に呼び出すようにしています。
実際の動きはこちらになります。
TextFieldの入力状況でdisabledを制御する
次は、1文字も入力されていない時には、ボタンを押せないようにします。
struct InputView: View {
@State var inputText = ""
@State var outputText = ""
@ObservedObject var keyboard = KeyboardService()
var body: some View {
ZStack {
VStack {
Text(outputText)
TextField(
"入力してね",
text: $inputText,
onEditingChanged: { editing in
}).padding()
}
FloatingButton(
bottomPadding: self.keyboard.keyboardHeight,
tappedHandler: self.showText,
isDisabled: inputText.isEmpty // --- 1
)
}.onAppear {
self.keyboard.start()
}.onDisappear {
self.keyboard.end()
}
}
func showText() {
outputText = inputText
}
}
struct FloatingButton: View {
var bottomPadding: CGFloat = 0
var tappedHandler: (() -> Void)? = nil
var isDisabled: Bool = false // --- 2
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
self.tappedHandler?()
}, label: {
Image(systemName: "pencil")
.foregroundColor(.white)
.font(.system(size: 24))
})
.frame(width: 60, height: 60)
.background(backgroundColor) // --- 3
.cornerRadius(30.0)
.shadow(color: .gray, radius: 3, x: 3, y: 3)
.padding(EdgeInsets(top: 0, leading: 0, bottom: bottomPadding + 16.0, trailing: 16.0))
.animation(.easeOut)
.disabled(isDisabled) // --- 4
}
}
}
// --- 5
var backgroundColor: Color {
return isDisabled ? Color.gray : Color.orange
}
}
- 1, 2:
inputText
が空だった場合は、isDisabled
変数をtrue
にする。そうでなければfalse
- 4: 変数
isDisabled
の値でdisableにするか否かを決定 - 3, 5: disableの時はグレーアウトするように背景色を変更
実際の動きはこちらになります。
おわりに
SwiftUIおもしろー。OSSになったら良いのになぁ。