SwiftUI button - only right side tappable - button

I have the following SwiftUI view which is essentially a textfield containing a button:
var body: some View {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: Constants.cornerRadius, style: .continuous)
.stroke(isFocused ? Color.blue : hasWarning ? .blue : .gray, lineWidth: Constants.lineWidth)
HStack {
Text(title)
.font(.bodyFont)
.foregroundColor(isFocused ? Color.blue : hasWarning ? .red : .gray)
.padding(.horizontal, text.isEmpty ? Constants.Padding.horizontalIsEmpty : Constants.Padding.horizontalIsNotEmpty)
.background(text.isEmpty ? Color.clear : background)
.padding(.leading, Constants.Padding.leading)
.offset(y: text.isEmpty ? Constants.Offset.isEmpty : -(frameHeight / Constants.Offset.isNotEmptyRatio))
.scaleEffect(text.isEmpty ? Constants.ScaleEffect.isEmpty : Constants.ScaleEffect.isNotEmpty, anchor: .leading)
if let button = button {
Spacer()
Button(action: {
button.action()
}, label: {
Text(button.labelText)
})
.padding(.trailing, Constants.Button.trailingPadding)
}
}
TextField("", text: $text, onEditingChanged: { inFocus in
self.isFocused = inFocus
})
.font(.bodyFont)
.padding(.horizontal, Constants.Padding.horizontal)
}
.frame(height: frameHeight)
.background(background)
.animation(.easeInOut(duration: Constants.Animation.easeInOut))
.padding(.top, Constants.Padding.top)
}
The result is a field like this:
However, only the very right hand side of the button is tappable.
I have tried adding a contentShape, like this:
if let button = button {
Spacer()
Button(action: {
button.action()
}, label: {
Text(button.labelText)
})
.contentShape(Rectange())
.padding(.trailing, Constants.Button.trailingPadding)
}
However this has made no difference. Wondering where I'm going wrong here.

Found the issue! It was a simple mistake.
The HStack including the button needed to be wrapped around this element:
TextField("", text: $text, onEditingChanged: { inFocus in
self.isFocused = inFocus
})
.font(.bodyFont)
.padding(.horizontal, Constants.Padding.horizontal)
Previously what was happening was that this element was overlapping the button, obscuring the tappable area. So the correct code would be:
HStack {
TextField("", text: $text, onEditingChanged: { inFocus in
self.isFocused = inFocus
})
.font(.bodyFont)
.padding(.horizontal, Constants.Padding.horizontal)
if let button = button {
Spacer()
Button(action: {
button.action()
}, label: {
Text(button.labelText)
})
.padding(.trailing, Constants.Button.trailingPadding)
}
}

Related

How to create a button that can be pressed repeatedly for new views? [SwiftUI]

I have a button ("Next Word") that when pressed shows a new view ("PracticeResult"). The idea is that everytime you press it, a new random word appears on the screen. But so far, I have to press the button twice to get it to show the next image because it's triggered with a boolean state.
I've tried changing the State back to false with an ".onAppear" toggle but it doesn't work. I've also tried using an origin State to toggle the variable back to false but it hasn't worked either. I'm quite new to SwiftUI so any tips would be appreciated! Thanks in advance.
struct PracticeView: View {
#State var isTapped: Bool = false
var body: some View {
NavigationView {
ZStack {
Color.white
VStack() {
Image("lightlogolong")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300.0, height: 100.0)
.cornerRadius(100)
.animation(.easeIn, value: 10)
NavigationLink(destination:
ContentView().navigationBarBackButtonHidden(true) {
HomeButton()
}
Group {
if (isTapped == true){
PracticeResult()
}
}.onAppear{
isTapped.toggle()
}
Button("Next Word", action:{
self.isTapped.toggle()
})
.padding()
.frame(width: 150.0, height: 40.0)
.font(.title2)
.accentColor(.black)
.background(Color("appblue"))
.clipShape(Capsule())
}
}
}
}
}
You could try this simple approach, using a UUID or something like it,
to trigger your PracticeResult view every time you tap the button. Note in particular the PracticeResult().id(isTapped)
struct PracticeView: View {
#State var isTapped: UUID? // <--- here
var body: some View {
NavigationView {
ZStack {
Color.white
VStack() {
Image("lightlogolong")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300.0, height: 100.0)
.cornerRadius(100)
.animation(.easeIn, value: 10)
NavigationLink(destination:
ContentView().navigationBarBackButtonHidden(true) {
HomeButton()
}
if isTapped != nil {
PracticeResult().id(isTapped) // <--- here
}
Button("Next Word", action:{
isTapped = UUID() // <--- here
})
.padding()
.frame(width: 150.0, height: 40.0)
.font(.title2)
.accentColor(.black)
.background(Color.blue)
.clipShape(Capsule())
}
}
}
}
}
// for testing
struct PracticeResult: View {
#State var randomWord: String = UUID().uuidString
var body: some View {
Text(randomWord)
}
}
if you want your button trigger a new random word appears on the screen, you should use button to change that random word
for example (you can try it on Preview):
struct PracticeView: View {
let words = ["one","two","three"]
#State var wordToDisplay: String?
var body: some View {
VStack {
if let wordToDisplay = wordToDisplay {
Text(wordToDisplay)
}
Spacer()
Button("Next Word") {
wordToDisplay = words.filter { $0 != wordToDisplay }.randomElement()
}
}.frame(height: 50)
}
}

SwiftUI - How to perform action only while the button is tapped, and end it when the tap is released?

I'm trying to recreate a Game Boy like game pad in SwiftUI. Graphically it looks good, but I can't make the actions work. I would like it to perform the action (move in the selected direction) while the arrow is tapped, and to stop moving once the arrow isn't tapped anymore (just like a real game pad would). The code I tried so far is this one:
import SwiftUI
struct GamePad: View {
#State var direction = "Empty"
#State var animate = false
var body: some View {
ZStack {
VStack {
Text("\(direction) + \(String(describing: animate))")
.padding()
Spacer()
}
VStack(spacing: 0) {
Rectangle()
.frame(width: 35, height: 60)
.foregroundColor(.gray.opacity(0.3))
.overlay(
Button {
direction = "Up"
animate = true
} label: {
VStack {
Image(systemName: "arrowtriangle.up.fill")
.foregroundColor(.black.opacity(0.4))
Spacer()
}
.padding(.top, 10)
.gesture(
TapGesture()
.onEnded({ () in
direction = "Ended"
animate = false
})
)
}
)
Rectangle()
.frame(width: 35, height: 60)
.foregroundColor(.gray.opacity(0.3))
.overlay(
Button {
direction = "Down"
animate = true
} label: {
VStack {
Spacer()
Image(systemName: "arrowtriangle.down.fill")
.foregroundColor(.black.opacity(0.4))
}
.padding(.bottom, 10)
.gesture(
TapGesture()
.onEnded({ () in
direction = "Ended"
animate = false
})
)
}
)
}
HStack(spacing: 35) {
Rectangle()
.frame(width: 43, height: 35)
.foregroundColor(.gray.opacity(0.3))
.overlay(
Button {
direction = "Left"
animate = true
} label: {
VStack {
Image(systemName: "arrowtriangle.left.fill")
.foregroundColor(.black.opacity(0.4))
Spacer()
}
.padding(.top, 10)
.gesture(
TapGesture()
.onEnded({ () in
direction = "Ended"
animate = false
})
)
}
)
Rectangle()
.frame(width: 43, height: 35)
.foregroundColor(.gray.opacity(0.3))
.overlay(
Button {
direction = "Right"
animate = true
} label: {
VStack {
Spacer()
Image(systemName: "arrowtriangle.right.fill")
.foregroundColor(.black.opacity(0.4))
}
.padding(.bottom, 10)
.gesture(
TapGesture()
.onEnded({ () in
direction = "Ended"
animate = false
})
)
}
)
}
}
}
}
What am I doing wrong? Thanks
It can be achieved with custom button style, because it has isPressed state in configuration.
Here is a demo of possible solution. Tested with Xcode 13.4 / iOS 15.5
struct StateButtonStyle: ButtonStyle {
var onStateChanged: (Bool) -> Void
func makeBody(configuration: Configuration) -> some View {
configuration.label
.opacity(configuration.isPressed ? 0.5 : 1) // << press effect
.onChange(of: configuration.isPressed) {
onStateChanged($0) // << report if pressed externally
}
}
}
and updated button with it
Button {
direction = "Ended" // action on touchUP
} label: {
VStack {
Image(systemName: "arrowtriangle.up.fill")
.foregroundColor(.black.opacity(0.4))
Spacer()
}
.padding(.top, 10)
}
.buttonStyle(StateButtonStyle { // << press state is here !!
animate = $0
if $0 {
direction = "Up"
}
})
Test module on GitHub

Want to show zoom in zoom out on map through buttons in swiftUI

I want to add 2 buttons on a map in swiftUI Map to zoom in and out of it. I have positioned the two buttons at the bottom of the screen in a Stack inside a VStack. I need the code to make them work.
This is what I have:
import Foundation
import SwiftUI
import MapKit
struct QuakeDetail: View {
var quake: Quake
#State private var region : MKCoordinateRegion
init(quake : Quake) {
self.quake = quake
_region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
span: MKCoordinateSpan(latitudeDelta: 3, longitudeDelta: 3)))
}
var body: some View {
ZStack {
VStack {
Map(coordinateRegion: $region, annotationItems: [quake]) { item in
MapMarker(coordinate: item.coordinate, tint: .red)
} .ignoresSafeArea()
HStack {
Button {
print ("pressed +")
} label: {
HStack {
Text("map")
Image(systemName: "plus")
}
}.padding(5).border(Color.blue, width: 1)
Spacer()
QuakeMagnitude(quake: quake)
Spacer()
Button {
print ("pressed -")
} label: {
HStack {
Text("map")
Image(systemName: "minus")
}
}.padding(5).border(Color.blue, width: 1)
}.padding()
Text(quake.place)
.font(.headline)
.bold()
Text("\(quake.time.formatted())")
.foregroundStyle(Color.secondary)
Text("\(quake.latitude) \(quake.longitude)")
}
}
}
}
You have to operate with span parameter of MKCoordinateRegion for that (the less value the more zoomed area), like (demo zoom on 10%):
Button("Zoom In") {
region.span.latitudeDelta *= 0.9
region.span.longitudeDelta *= 0.9
}
Tested with Xcode 13.2 / iOS 15.2

Init Custom Button SwiftUI

Trying to init CustomButton(title: "Add", icon: .add, status: .enable)
My code is below. I do get the title but enums are not working.
Plus recieving error
Cannot convert value of type 'Image' to expected argument type 'String'
at Image(icon)
import SwiftUI
struct CustomButton: View {
var title: String
var icon: String
var status: Color
var body: some View {
Button(action: {
}) {
Text(title)
.foregroundColor(.white)
.background(Color(.green))
.font(Font.custom("SFCompactDisplay", size: 14))
Image(icon)
.renderingMode(.original)
.foregroundColor(.white)
}
}
enum Icon {
case add
case edit
var image: Image {
switch self {
case .add:
return Image("Add")
case .edit:
return Image("Edit")
}
}
}
enum Status {
case enable
case disable
var color : Color {
switch self {
case .enable:
return Color(.green)
case .disable:
return Color(.gray)
}
}
}
init(title: String, icon: Icon, status: Status) {
self.title = title
self.icon = icon.image
self.status = status.color
}
}
I assume you wanted this
struct CustomButton: View {
var title: String
var icon: Icon
var status: Color
var body: some View {
Button(action: {
}) {
Text(title)
.foregroundColor(.white)
.background(Color(.green))
.font(Font.custom("SFCompactDisplay", size: 14))
icon.image
.renderingMode(.original)
.foregroundColor(.white)
}
}
enum Icon {
case add
case edit
var image: Image {
switch self {
case .add:
return Image("Add")
case .edit:
return Image("Edit")
}
}
}
enum Status {
case enable
case disable
var color : Color {
switch self {
case .enable:
return Color(.green)
case .disable:
return Color(.gray)
}
}
}
init(title: String, icon: Icon, status: Status) {
self.title = title
self.icon = icon
self.status = status.color
}
}
I figured it out. It works now.
struct CustomButton: View {
let title: String
let icon : String
let status: Color
#State private var buttonDisabled = true
var body: some View {
Button(action: {
}) {
ZStack(alignment:.bottom) {
HStack {
Text(title)
.foregroundColor(.white)
.font(Font.custom("SFCompactDisplay-Bold", size: 20))
.bold()
.fontWeight(.bold)
.background(status)
Image(icon)
.renderingMode(.original)
.foregroundColor(.white)
.background(Color(.white))
}
.frame(width: 335, height: 20, alignment: .center)
.padding()
.background(status)
}
.cornerRadius(10)
}
}
enum Icon {
case add
case edit
case none
var image: String {
switch self {
case .add:
return "Add"
case .edit:
return "Edit"
case .none:
return "empty"
}
}
}
enum Status {
case enable
case disable
}
init(title: String, icon: Icon, status: Status) {
self.title = title
self.icon = icon.image
if status == .enable {
self.status = Color(#colorLiteral(red: 0, green: 0.6588235294, blue: 0.5254901961, alpha: 1))
} else {
self.status = Color(#colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1))
}
}
}
struct CustomButton_Previews: PreviewProvider {
static var previews: some View {
CustomButton(title: "Odeme Yontemi Ekle", icon: .none, status: .enable)
}
}

Why does my Button style object only work on the text within the button and not the entire button?

I have below a ButtonStyle object that I wish to use when a Button is tapped. However, when I call it on my Button, the effect is seen only on the text within the button and not on the entire Button as a whole.
I have tried calling it outside but to no avail. Can someone please explain to me how I can fix my error?
Button(action: {
}) {
Text("Button")
.foregroundColor(.white)
}.background(Color.red)
.buttonStyle(ScaleButtonStyle())
struct ScaleButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 5 : 1)
}
}
EDIT:
Button(action: {}) {
Text("Button")
.fontWeight(.semibold)
.foregroundColor(.white)
.font(.title)
}
.buttonStyle(ScaleButtonStyle(bgColor: Color.red))
.frame(width: geo.size.width*0.8, height: geo.size.height*0.1, alignment: .center)
.background(Color.red)
.clipShape(Rectangle())
struct ScaleButtonStyle: ButtonStyle {
let bgColor: Color
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(bgColor)
.scaleEffect(configuration.isPressed ? 5 : 1)
}
}
The solution is to make background as part of a button style as shown below.
Tested with Xcode 11.4 / iOS 13.4
struct ScaleButtonStyle: ButtonStyle {
let bgColor: Color
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(bgColor)
.scaleEffect(configuration.isPressed ? 5 : 1)
}
}
struct TestScaleButtonStyle: View {
var body: some View {
Button(action: { }) {
Text("Button")
.foregroundColor(.white)
}.buttonStyle(ScaleButtonStyle(bgColor: Color.red))
}
}

Resources