SwiftUI: Optimal way to disable NavigationLink which has a custom ButtonStyle - swift

I have a navigation link which has a custom ButtonStyle:
NavigationLink(destination: NextScreen()) {
Text("Next")
}
.buttonStyle(CustomButtonStyle(disabled: !isValidPassword))
And my CustomButtonStyle looks like this:
#State var disabled = false
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(minWidth: 0, maxWidth: .infinity)
.padding(15)
.foregroundColor(.white)
.background(disabled ? .black : .gray)
.cornerRadius(40)
.disabled(disabled) // this has no effect when inside a NavigationLink
}
The UI updates correctly as the user types in the password.
You can see that I disable the button inside the ButtonStyle, but this doesn't prevent the user from still tapping the NavigationLink to go to NextScreen().
To fix this I end up doing this:
NavigationLink(destination: SignupStepBirthdayView()) {
Text("Next")
}
.buttonStyle(BobbleUpButtonStyle(disabled: !isValidPassword))
.disabled(!isValidPassword)
Which seems inefficient as I'm passing a disabled state to the button style to update the UI, and then having to disable the actual NavigationLink.
Is there a better way to do this?

I will show you simple way, no needed to using #Bingding or #State
First, create your button style:
struct CustomButtonStyle: ButtonStyle {
public func makeBody(configuration: ButtonStyle.Configuration) -> some View {
MyButton(configuration: configuration)
}
struct MyButton: View {
let configuration: ButtonStyle.Configuration
#Environment(\.isEnabled) private var isEnabled: Bool
var body: some View {
configuration.label
.frame(minWidth: 0, maxWidth: .infinity)
.padding(15)
.foregroundColor(.white)
.background(isEnabled ? Color.blue : Color.gray)
.cornerRadius(40)
.disabled(!isEnabled)
}
}
}
Then, disable it as any SwiftUI component:
struct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter your text", text: $text)
NavigationLink(destination: NextScreen()) {
Text("Next")
}
.buttonStyle(CustomButtonStyle())
.disabled(text.isEmpty)
}
}
}
}

You have to use Binding instead of State in CustomButtonStyle
#Binding var disabled : Bool
And this changes in your ContentView
#State var isValidPassword: Bool = true
also:
.buttonStyle(BobbleUpButtonStyle(disabled: $isValidPassword))

Related

SwiftUI: FocusState animations stop working when using NavigationViews

I am trying to implement an animation relating to a TextField. A Cancel button slide in when the textfield is clicked. However, it only works correctly when it's in a standalone view. When I try to nest the view within a NavigationLink, the animation stops working. Here's the code:
struct TestView: View {
#FocusState private var isEditing: Bool
var body: some View {
VStack {
Button("click me", action: { isEditing.toggle() })
HStack {
TextField("Search", text: .constant("test"))
.focused($isEditing)
.padding(8)
.padding(.leading, 25)
.padding(.trailing, 22)
.background(Color.gray)
.cornerRadius(10)
.padding(.horizontal)
if isEditing {
Button {} label: {
ZStack {
Text("Cancel")
.foregroundColor(.primary)
.padding(.trailing)
}
}
.transition(.move(edge: .trailing))
}
}
.animation(.spring(), value: isEditing)
.navigationBarHidden(true)
}
}
}
Correct animation:
https://imgur.com/iqGr7fx
However, when I have a second view with a NavigationLink containing the previous view:
struct TestView2: View {
#State var test: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(isActive: $test, destination: { TestView() }, label: {})
Button("click me", action: { test.toggle() })
}
.navigationBarHidden(true)
}
}
}
The animation looks like this:
https://imgur.com/a/LK9pxf2
Is this a bug related to SwiftUI? Or am I not supposed to use FocusState for animations? If so, how can I change the code to have the animation work in both versions?
Fixed the animations by changing the FocusState variable to a State variable and then removing the .focused modifier and changing it to .onTapGesture, setting the isEditing variable to true

Assign the value of ButtonStyle configuration.isPressed to other variable

I have 2 questions, first, I have this code and I'm trying to assign the value of configuration.isPressed (in ButtonStyle ) to the variable #State var isPressed
import SwiftUI
import ARKit
struct ContentView: View {
#State var isPressed: Bool = false
var body: some View {
ZStack {
VStack {
Spacer()
Button(action: {}) {
Text("Hello World")
}
.buttonStyle(MyButtonStyle(p: $isPressed))
}
}
}
}
struct MyButtonStyle: ButtonStyle {
#Binding var p: Bool
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(8.0)
if configuration.isPressed {
////CODE HERE ?
}
}
}
Is the above possible?
Second questions: I found plenty of ways to detect when a button is pressed and released. Is the usage ButtonStyle a good practice/way to achieve this goal?
Thanks in advance!

Text() in front of TextField() blocking editing in SwiftUI

So I'd like my textfield to have a customizable placeholder text so I decided to put a Text() element in a ZStack in front of the text field. The only problem is, this Text() item blocks the selection of the textfield that is behind it (AKA when I click the placeholder I want the TextField to be clicked). Unfortunately, this Text() element blocks the click. I tried using the .allowsHitTesting() property as seen below but that also didn't work, and I'm not sure why.
struct ContentView: View {
#State var text: String = ""
var body: some View {
ZStack {
TextField("", text: self.$text)
.background(Color.red)
.foregroundColor(Color.white)
if text.isEmpty {
Text("Placeholder")
.allowsHitTesting(false)
}
}
}
}
It can be done with custom text field style.
Here is a demo of solution (or parameters can be tuned). Tested with Xcode 12 / iOS 14 (border is just for visibility)
struct PlaceholderStyle: TextFieldStyle {
let isActive: Bool
var placeholder = "Placeholder"
var color = Color.white
var backgrond = Color.red
func _body(configuration: TextField<_Label>) -> some View {
Text("\(isActive ? placeholder : "")")
.foregroundColor(isActive ? color : .clear)
.background(isActive ? backgrond : .clear)
.frame(maxWidth: .infinity, alignment: .leading)
.overlay(configuration)
}
}
struct DemoView: View {
#State private var text = ""
var body: some View {
TextField("", text: $text)
.border(Color.gray).padding(.horizontal)
.textFieldStyle(PlaceholderStyle(isActive: text.isEmpty))
}
}
See if this fits your needs:
struct ContentView: View {
#State var text = ""
var body: some View {
ZStack(alignment: .leading) {
if text.isEmpty { Text("Placeholder")
.foregroundColor(.red)
.background(Color.yellow)
}
TextField("", text: $text)
.background(text.isEmpty ? Color.clear : Color.yellow)
}
}
}

SWIFTUI - How to keep score in a quiz app with questions on separate views?

Newbie here. I am creating a quiz app. Unlike most of the examples, every question is in a separate view. I'd like to keep the score and show it at the end.
Example:
struct questionOne: View {
#State var isSelected = false
var body: some View {
GeometryReader { geometryProxy in
VStack(alignment: .center) {
TRpic().cornerRadius(10)
Text("What's the capital of Turkey?")
.font(.title)
Spacer()
Button(action: {self.isSelected.toggle()}) {
Text("Istanbul")
} .buttonStyle(SelectedButtonStyleFalse(isSelected: self.$isSelected))
Button(action: {self.isSelected.toggle()}) {
Text("Ankara")
}.buttonStyle(SelectedButtonStyle(isSelected: self.$isSelected))
Button(action: {self.isSelected.toggle()}) {
Text("Athens")
} .buttonStyle(SelectedButtonStyleFalse(isSelected: self.$isSelected))
Spacer()
NavigationLink(destination: questionTwo()) {
VStack {
Text("Next Question")
Adview().frame(width: 150, height: 50)
}
}
}
}
}
}
struct SelectedButtonStyle: ButtonStyle {
#Binding var isSelected: Bool
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(20)
.foregroundColor(.white)
.background(isSelected ? Color.green : Color.gray)
.cornerRadius(10.0)
}
}
struct SelectedButtonStyleFalse: ButtonStyle {
#Binding var isSelected: Bool
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(20)
.foregroundColor(.white)
.background(isSelected ? Color.red : Color.gray)
.cornerRadius(10.0)
}
}
I have two button styles: SelectedButtonStyle (for the true answer) and SelectedButtonStyleFalse (for the false answer)
All 50 questions (views) are connected via NavigationView. And in the end, I want to show the total score in a separate view. For example "You have scored 35/50".
Thanks!
You can use #EnvironmentObject.
All you have to do it create a class like this:
import Foundation
class GameStatus: ObservableObject {
#Published var score: Int = 0
}
Then in your struct add #EnvironmentObject var gameStatus: GameStatus
Finally, you need to trigger update on the score, just do
self.gameStatus.answers += 1
Check out Three Ways To Do The Same Job for 3 examples of how to share data between views.

Changing the color of a button in SwiftUI based on disabled or not

I have a textfield with a send button that's a systemImage arrow. I want the foreground color of the image to change depending on whether the textField is empty or not. (I.e. the button is gray, and it is disabled if the textfield is empty. It's blue if the count of the textfield text is > 1).
I have a workaround that's not perfect:
if chatMessageIsValid {
Spacer()
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.padding(.leading, 10)
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
.foregroundColor(Color.blue)
.padding(.trailing, 10)
}.disabled(!chatMessageIsValid)
}
} else {
Spacer()
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.padding(.leading, 10)
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
.foregroundColor(Color.gray)
.padding(.trailing, 10)
}.disabled(!chatMessageIsValid)
}
}
This almost works, and it does change the color of the image if the text is > 1 in length. However, due to the change in state you're kicked out of editing the textfield after one character is typed, and you'll need to select the textfield again to continue typing. Is there a better way to do this with the .disabled modifier?
I guess you want this:
You can add a computed property for the button color, and pass the property to the button's foregroundColor modifier. You can also use a single padding modifier around the HStack instead of separate paddings on its subviews.
struct ContentView : View {
#State var chatMessage: String = ""
var body: some View {
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
.foregroundColor(buttonColor)
}
.disabled(!chatMessageIsValid)
}
.padding([.leading, .trailing], 10)
}
var chatMessageIsValid: Bool {
return !chatMessage.isEmpty
}
var buttonColor: Color {
return chatMessageIsValid ? .accentColor : .gray
}
func sendMessage() {
chatMessage = ""
}
}
However, you shouldn't use the foregroundColor modifier at all here. You should use the accentColor modifier. Using accentColor has two benefits:
The Image will automatically use the environment's accentColor when the Button is enabled, and gray when the Button is disabled. You don't have to compute the color at all.
You can set the accentColor in the environment high up in your View hierarchy, and it will trickle down to all descendants. This makes it easy to set a uniform accent color for your whole interface.
In the following example, I put the accentColor modifier on the HStack. In a real app, you would probably set it on the root view of your entire app:
struct ContentView : View {
#State var chatMessage: String = ""
var body: some View {
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
}
.disabled(!chatMessageIsValid)
}
.padding([.leading, .trailing], 10)
.accentColor(.orange)
}
var chatMessageIsValid: Bool {
return !chatMessage.isEmpty
}
func sendMessage() {
chatMessage = ""
}
}
Also, Matt's idea of extracting the send button into its own type is probably smart. It makes it easy to do nifty things like animating it when the user clicks it:
Here's the code:
struct ContentView : View {
#State var chatMessage: String = ""
var body: some View {
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.textFieldStyle(.roundedBorder)
SendButton(action: sendMessage, isDisabled: chatMessage.isEmpty)
}
.padding([.leading, .trailing], 10)
.accentColor(.orange)
}
func sendMessage() {
chatMessage = ""
}
}
struct SendButton: View {
let action: () -> ()
let isDisabled: Bool
var body: some View {
Button(action: {
withAnimation {
self.action()
self.clickCount += 1
}
}) {
Image(systemName: "arrow.up.circle")
.rotationEffect(.radians(2 * Double.pi * clickCount))
.animation(.basic(curve: .easeOut))
}
.disabled(isDisabled)
}
#State private var clickCount: Double = 0
}
With these various solutions, you can use the \.isEnabled environment property instead of creating custom button styles or passing in disabled booleans yourself.
#Environment(\.isEnabled) private var isEnabled
If you want to customize the button even further with disabled (or any other) state, you can add variables to your custom ButtonStyle struct.
Swift 5.1
struct CustomButtonStyle: ButtonStyle {
var disabled = false
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(disabled ? .red : .blue)
}
}
// SwiftUI
#State var isDisabled = true
var body: some View {
Button().buttonStyle(CustomButtonStyle(disabled: self.isDisabled))
}
Try this
Spacer()
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.padding(.leading, 10)
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
.foregroundColor(chatMessageIsValid ? Color.blue : Color.gray)
.padding(.trailing, 10)
}.disabled(!chatMessageIsValid)
}
I suppose, the reason TextField loses focus is that in your code the whole view hierarchy is changed depending on the value of chatMessageIsValid. SwiftUI doesn't understand that the view hierarchy in the then block is almost identical to the one in the else block, so it rebuilds it completely.
In this edited code it should see that the only thing that changes is the foregroundColor of the image and the disabled status of the button, leaving the TextField untouched. Apple had a similar examples in one of the WWDC videos, I believe.
All modifier arguments can be conditional:
.foregroundColor(condition ? .red : .yellow)
where the condition could be any true/false
var condition: Bool { chatMessage.isEmpty } // can be used inline
You can use any other condition you need