Group buttons and change icon color on active selection - swift

So I have a few buttons inside a VStack() which I want to bind or combine with one state property, to determine when each button was clicked.
When a button is clicked, I want to use the .accentColor modifier while the action is active and when the action is done, fallback to the original Color.secondary.
Currently, here is what it's doing:
- When app is opened, it loads the default color.
- When one button is clicked, it turns both buttons into a lighter shade of gray.
- When I exit the view, it turns both buttons to accentColor and keeps them like that.
- When I click on a button again, it turns gray.
Video for reference:
https://im5.ezgif.com/tmp/ezgif-5-d53f89a9af.gif
Could anyone spot what I might be doing wrong?
So I have the following code (Code has been stripped of all the unnecessary items, such as var body: some View, etc..):
#State private var tappedActiveMapAnnotationButton : Bool = false
VStack {
Button(action: {
withAnimation(.spring()) {
self.tappedActiveMapAnnotationButton = true
mainViewModel.showFilters.toggle()
}
}, label: {
Image(systemName: "slider.horizontal.3")
.font(.title3)
.padding(10)
.foregroundColor(tappedActiveMapAnnotationButton ? .accentColor : Color.secondary)
}) //: Button
Button(action: {
withAnimation(.spring()) {
self.tappedActiveMapAnnotationButton = true
showMapDisplaySheet.toggle()
}
}, label: {
Image(systemName: "map")
.font(.title3)
.padding(10)
.foregroundColor(tappedActiveMapAnnotationButton ? .accentColor : Color.secondary)
}) //: Button
.sheet(isPresented: $showMapDisplaySheet) {
MapDisplaySheetView()
.presentationDetents([.fraction(0.25)])
}
} //: VStack
.background {
RoundedRectangle(cornerRadius: 8)
.fill(Color(uiColor: .systemBackground))
}

Create the following enum
enum buttonSelection {
case buttonNone, button1, button2
}
and you essentially create a single state variable of the type buttonSelection and perform whatever you want based on the current value of buttonSelection
#State private var currentSelection : buttonSelection = .buttonNone
VStack {
Button(action: {
withAnimation(.spring()) {
self.currentSelection = .button1
mainViewModel.showFilters.toggle()
}
}, label: {
Image(systemName: "slider.horizontal.3")
.font(.title3)
.padding(10)
.foregroundColor(currentSelection == .button1 ? .accentColor : Color.secondary)
}) //: Button
Button(action: {
withAnimation(.spring()) {
self.currentSelection = .button2
showMapDisplaySheet.toggle()
}
}, label: {
Image(systemName: "map")
.font(.title3)
.padding(10)
.foregroundColor(currentSelection == .button2 ? .accentColor : Color.secondary)
}) //: Button
.sheet(isPresented: $showMapDisplaySheet, onDismiss: {
print("Dismissed")
currentSelection = .buttonNone
}) {
MapDisplaySheetView()
.presentationDetents([.fraction(0.25)])
}
} //: VStack
.background {
RoundedRectangle(cornerRadius: 8)
.fill(Color(uiColor: .systemBackground))
}

Related

Blur view when button is pressed swiftui

I have a view with a toolbar on the bottom of the view. When clicked - two buttons are displayed. I am trying to achieve when the toolbar is pressed and the buttons are now displayed, the view (or background) becomes blurred/grayed out, except for the newly produced items.
I attached a screenshot of the desired effect I am aiming for.
struct UserDashController: View {
// #State private var showMealView = false
#State private var showSettingsView = false
#State private var showAddViews = false
#State private var angle: Double = 0
init(){
UIToolbar.appearance().barTintColor = UIColor.white
}
var body: some View {
NavigationView {
VStack{
Text("Blue me Please")
.frame(width: 400, height:600)
.background(.orange)
}
//sets setting bar top right
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
VStack{
Button(action: {
showSettingsView.toggle()
}) {
Image(systemName: "line.3.horizontal")
.font(.title3)
.foregroundColor(.black)
}
.sheet(isPresented: $showSettingsView){
JournalEntryMain()
}
}
}
// sets add meal option bottom/center
ToolbarItem(placement: .bottomBar) {
//displaying add meal and recipe icons when clicked
HStack{
Button(action: {
angle += 90
showAddViews.toggle()
}) {
if showAddViews {
VStack{
AddToolbar(showAddOptions: $showAddViews)
.offset(y:-50)
}
}
Image(systemName: "plus.square")
.opacity(showAddViews ? 0.5 : 1)
.font(.largeTitle)
.foregroundColor(.black)
.rotationEffect(.degrees(angle))
.animation(.easeIn(duration: 0.25), value: angle)
}
}
}
}
}
}
}
Buttons that appear when toolbar is pressed
struct AddToolbar: View {
#Binding var showAddOptions: Bool
#State var showMealView = false
var body: some View {
HStack{
VStack{
Button(action: {
showMealView.toggle()
}){
VStack{
Image(systemName: "square.and.pencil")
.font(.title)
.foregroundColor(.black)
.background(Circle()
.fill(.gray)
.frame(width:50, height:50))
.padding(3)
Text("Meal")
.foregroundColor(.black)
}
}.fullScreenCover(isPresented: $showMealView){
JournalEntryMain()
}
}
VStack{
Image(systemName: "text.book.closed")
.foregroundColor(.black)
.font(.title)
.background(Circle()
.fill(.gray)
.frame(width:50, height:50))
.padding(3)
Text("Recipe")
.foregroundColor(.black)
}
.offset(y: -50)
}
.frame(height:150)
}
}
Desired Effect
I'm a little confused by your desired effect example, partially because in the UI screenshot you attached, the background isn't blurred, it's just darkened. So, the following answer isn't tailored to your specific example but still should be able to help.
Let's say whatever variable you're using to determine whether or not to show the toolbar is showSettingsView. You could put the following modifiers on your background view:
To blur: .blur(showSettingsView ? 0.5 : 0.0)
To darken: .brightness(showSettingsView ? -0.5 : 0.0)
Obviously just replace "0.5" with whatever number feels best.

Delay transition of a button

I'm attempting to transition a button in a SwiftUI project such that, when the button is pressed, it will do a .move(edge: .trailing) and after approximately a half a second a different image for the button will come into view.
Here's what I have so far and as expected the image changes simultaneously. I'm curious if this is something that can be accomplished with asymmetric transitions. Trying to avoid using two different buttons that have animating offsets changing.
#State var delayedMove = false
#ViewBuilder
var moveMe: some View {
HStack {
Spacer()
Button(action: {
withAnimation {
delayedMove.toggle()
}
}) {
if delayedMove {
selectImageButton
.animation(.linear.delay(delayedMove ? 0 : 1))
.transition(.move(edge: .trailing))
} else {
deleteImage
.animation(.linear.delay(!delayedMove ? 0 : 1))
.transition(.move(edge: .trailing))
}
}
.padding()
}
}
Yes you can use asymmetric, and no need to repeat the code. But Transition in SwiftUI needs 2 things to works correctly first a Group and Second an if & else, it is how it works right now maybe in future it changes but you can see the Code:
struct ContentView: View {
#State private var toggleButton = false
var body: some View {
HStack {
Button("Toggle Button") { toggleButton.toggle() }
Spacer()
Button(action: { toggleButton.toggle() }, label: {
Group {
if toggleButton {
Image(systemName: "trash")
}
else {
Image(systemName: "plus")
}
}
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: .trailing), removal: AnyTransition.move(edge: .trailing)))
})
}
.padding()
.animation(Animation.linear(duration: 0.5), value: toggleButton)
}
}
You are quite close. I just split up the single if-else into 2 ifs, wrapped in a ZStack.
The button also makes secondaryDelay equal true for half a second, and then it switches back to false.
When the view leaves the hierarchy, that's when the transition starts. I worked out the way to do this by considering all the scenarios for the selectImageButton:
Showing - show: delayedMove = true, secondaryDelay = false
Starting to hide - hide: delayedMove = false, secondaryDelay = true
Hiding - hide: delayedMove = false, secondaryDelay = false
Starting to show - hide: delayedMove = true, secondaryDelay = true
From above, we only need to show when delayedMove && !secondaryDelay equals true. Similar method for other deleteImage.
Code:
struct ContentView: View {
#State var delayedMove = false
#State var secondaryDelay = false
var body: some View {
moveMe
}
#ViewBuilder
var moveMe: some View {
HStack {
Spacer()
Button(action: {
withAnimation {
delayedMove.toggle()
secondaryDelay = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
secondaryDelay = false
}
}
}) {
ZStack {
if delayedMove && !secondaryDelay {
selectImageButton
.animation(.linear)
.transition(.move(edge: .trailing))
}
if !delayedMove && !secondaryDelay {
deleteImage
.animation(.linear)
.transition(.move(edge: .trailing))
}
}
}
.padding()
}
}
}
Result:
You can still use the same button and have the label animated with an offset if you want a clean and smooth animation like the first image going out and the other entering. you can try this view
#State var delayedMove = false
var moveMe: some View {
HStack {
Spacer()
Button(action: {
withAnimation (.linear(duration: 0.5)){
delayedMove.toggle()
}
}) {
ZStack{
Image(systemName: "trash")
.offset(x: delayedMove ? 0 : 200)
Image(systemName: "plus")
.offset(x: delayedMove ? 200 : 0)
}
.imageScale(.large)
}
.padding()
}
}

Custom action sheet with SwiftUI

I have a TabView at the top of navigation and a tab which contains the user profile.
I want to present an action sheet with custom style when users touch cover/profile picture.
The custom style looks like this:
I already try to attach a second view at the bottom of the ZStack but the TabView always looks in front.
How can I custom the action sheet or how can I hide TabView?
Thanks
ZStack approach will work in this scenario.
I have tried with a small example, you can tweak and try to fit in your project and see if it works.
Implementation :
struct ContentView: View {
#State var showingCustomSheet = false
var body: some View {
ZStack {
VStack {
Button(action: {
withAnimation {
self.showingCustomSheet.toggle()
}
}) {
Text("Show Custom sheet")
}
}
ZStack {
HStack(alignment: .bottom) {
Spacer()
VStack (alignment: .leading, spacing: 10) {
Spacer()
Button(action: {}) { Text("Delete photo") }
Button(action: {}) { Text("Take photo") }
Button(action: {}) { Text("Choose photo") }
Button(action: {
withAnimation {
self.showingCustomSheet.toggle()
}
}) { Text("Cancel") }
}
Spacer()
}
}.background(Color.black.opacity(0.1))
.edgesIgnoringSafeArea(.all)
.offset(x:0, y: self.showingCustomSheet ? 0 : UIApplication.shared.keyWindow?.frame.height ?? 0)
}
}
}

swiftui textfiled and button inside a button

I am trying to trigger button when a user clicks background area of a view.
As a solution, I decided to make a button and put the view into the button as a label.
But the Problem is that the button is triggered even on the TextField and sometimes on another button inside.
How can i disable the back button on some views (TextField and Button in this case) I select?
I attach my code below and rendered preview.
import SwiftUI
struct ButtonOnbutton: View {
#State var memo: String = ""
var body: some View {
Button(action: {
print("down button clicked")
}) {
VStack(spacing: 10) {
Text("before")
.foregroundColor(.white)
TextField("type memo", text: $memo)
.font(.custom("SFProDisplay-Regular", size: 12))
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("after")
.foregroundColor(.white)
Button(action: {
print("inside button clicked")
}) {
Text("inside button")
.foregroundColor(.white)
}
.background(Color.red)
}
}
.background(Color.blue)
.padding()
}
}
struct buttonOnbutton_Previews: PreviewProvider {
static var previews: some View {
ButtonOnbutton()
}
}
If I correctly understood your goal then it is better to used onTapGesture instead of button, as shown below
var body: some View {
VStack(spacing: 10) {
Text("before")
.foregroundColor(.white)
TextField("type memo", text: $memo)
.font(.custom("SFProDisplay-Regular", size: 12))
.textFieldStyle(RoundedBorderTextFieldStyle())
.onTapGesture {} // << just blocks outer tap
Text("after")
.foregroundColor(.white)
Button(action: {
print("inside button clicked")
}) {
Text("inside button")
.foregroundColor(.white)
}
.background(Color.red)
}
.background(Color.blue)
.onTapGesture {
print("background area clicked")
}
.padding()
}

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