SwiftUI ScrollView also triggering DragGesture - swiftui

I'm making swipeable flash cards that a user fills in with their own content. The card contains a vertical ScrollView (for text) and horizontal ScrollView (for tags). The card also has a drag gesture attached to it. However, when I'm scrolling through the ScrollViews it also triggers the DragGesture's .onChanged (but also doesn't even follow with .onEnded). How can I prioritize the ScrollView before the gesture?
struct ContentView: View {
#State private var translation: CGSize = .zero
var body: some View {
GeometryReader { geometry in
ZStack {
Color(.blue).edgesIgnoringSafeArea(.all)
VStack {
ScrollView {
Text("blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah")
}
Tags(decks: ["Tag", "Tag", "Tag", "Tag", "Tag", "Tag", "Tag"])
}
.padding(24)
.frame(width: geometry.size.width - 48, height: geometry.size.height / 2)
.background(Color.gray)
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
.animation(.interactiveSpring(response: 0.5, dampingFraction: 0.75, blendDuration: 0))
.offset(x: self.translation.width, y: self.translation.height)
.gesture(DragGesture()
.onChanged { value in
self.translation = value.translation
}.onEnded { value in
self.translation = .zero
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Tags: View {
var decks: [String] = []
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(decks, id: \.self) { deck in
Text(deck)
.font(.system(size: 12, weight: .semibold))
.padding(.vertical, 6)
.padding(.horizontal, 12)
.background(Color.white)
.clipShape(Capsule())
}
}
}
}
}

try using gesture modifiers, such as simultaneously(with:), sequenced(before:) or exclusively(before:) to address you problem.

Related

Initializing the Date Picker with Date Other Than Today

I need help with dates. How do you initialize the DatePicker with a stored date? Say, for example, that the user entered an entry with a date. He then determines the date is wrong and would like to change the date. But currently the DatePicker in this code will always default to today's date instead of the stored date.
The state parameter startDate can't be initialized with a stored date above the body. It appears that I need to set startDate in the body.
import SwiftUI
import Combine
struct GetDate: View {
#ObservedObject var userData : UserData = UserData()
#State private var startDate = Date()
var body: some View {
//let startDate = userData.startDate
VStack (alignment: .leading) {
Form {
Text("startdate 1 = \(startDate)")
// enter start date
Section(header: Text("Enter Start Date")) {
HStack {
Image(systemName: "calendar.badge.clock")
.resizable()
.foregroundColor(Color(.systemRed))
.frame(width: 35, height: 35)
Spacer()
DatePicker("", selection: $startDate, in: ...Date(), displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle())
}
}
Button ( action: {
userData.saveDates(startDate: startDate)
}) {
HStack {
Spacer()
Text("Save Dates?")
Spacer()
}
}
}
.font(.body)
.navigationBarTitle("Get Date", displayMode: .inline)
}
}
}
class UserData: ObservableObject {
#Published var startDate: Date = Date() {
didSet {
UserDefaults.standard.set(startDate, forKey: "startDate") // save
}
}
init() {
// save / retrieve trip dates
if let sdate = UserDefaults.standard.object(forKey: "startDate") as? Date {
startDate = sdate
print("startDate 2 = \(startDate)")
} else {
UserDefaults.standard.set(Date(), forKey: "startDate")
}
}
func saveDates(startDate: Date) -> () {
self.startDate = startDate
UserDefaults.standard.set(self.startDate, forKey: "startDate")
print("startDate 3 = \(self.startDate)")
}
}
Use #AppStorage property wrapper:
struct GetDate: View {
#AppStorage("startDate") var startDate = Date()
#State private var selectedDate = Date()
var body: some View {
VStack (alignment: .leading) {
Form {
Text("startdate 1 = \(startDate)")
Section(header: Text("Enter Start Date")) {
HStack {
Image(systemName: "calendar.badge.clock")
.resizable()
.foregroundColor(Color(.systemRed))
.frame(width: 35, height: 35)
Spacer()
DatePicker("", selection: $selectedDate, in: ...Date(), displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle())
}
}
Button ( action: {
startDate = selectedDate // save
}) {
HStack {
Spacer()
Text("Save Dates?")
Spacer()
}
}
}
.font(.body)
.navigationBarTitle("Get Date", displayMode: .inline)
}
.onAppear {
selectedDate = startDate
}
}
}
// Support #AppStorage for `Date` type.
extension Date: RawRepresentable {
private static let formatter = ISO8601DateFormatter()
public var rawValue: String {
Date.formatter.string(from: self)
}
public init?(rawValue: String) {
self = Date.formatter.date(from: rawValue) ?? Date()
}
}
Just reset startDate to userData.startDate in .onAppear().
.onAppear {
startDate = userData.startDate
}

I want to add $ sign to TextField in SwiftUI

Hi I want to add $ sign to a TextField when a user is typing.
This is my current code.
ZStack(alignment: .leading) {
if price.isEmpty {
Text("Enter total budget")
}
HStack {
TextField("", text: $price)
.keyboardType(.decimalPad)
}
}
currency formatter is the way to go, but if you just want to show a $ in the TextField as you type, you could use something like this:
(you can of course combine this approach with a Formatter)
struct ContentView: View {
#State var price = ""
var body: some View {
VStack {
ZStack(alignment: .leading) {
if price.isEmpty {
Text("Enter total budget")
}
HStack {
TextField("", text: Binding(
get: { price },
set: { newVal in
if price.starts(with: "$") {
price = newVal
} else {
price = "$" + newVal
}
})).keyboardType(.decimalPad)
}
}.padding(20)
Text("number entered: " + String(price.dropFirst()))
}
}
}

Background of Selected Item in Picker

I have a picker that looks like this:
With the code being this:
#State var typeSelection: String = "Socialism"
var typeOptions = ["Socialism", "Democratic Socialism", "Capitalism", "Authoritarianism"]
...
Picker("Please pick a Type", selection: $typeSelection) {
ForEach(typeOptions, id: \.self) {
Text($0)
.foregroundColor(.white)
.font(.system(size: 30, weight: .medium, design: .default))
}
}
How do I make the background color of the selected one darker, like if say it's opacity is 30% of a black color right now how do I bump that to 70%?

Why does SwiftUI format dates as "2020 M10 27" in navigationTitle?

I'm using Xcode 12.1 and the iOS 14.1 simulator.
With the simplest possible code using Text.init(_:style:), the date format seems to differ between the nav bar title and the content view:
struct ContentView: View {
var body: some View {
NavigationView {
Text(Date(), style: .date)
.navigationTitle(
Text(Date(), style: .date)
)
}
}
}
I'd expect the title and content to look like "October 27, 2020" but the title looks like "2020 M10 27". I've never seen this format before.
Using a custom Formatter with Text.init(_:formatter:) gives the same result:
struct ContentView: View {
var body: some View {
let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
NavigationView {
Text(Date(), formatter: formatter) // "October 27, 2020"
.navigationTitle(
Text(Date(), formatter: formatter) // "2020 M10 27"
)
}
}
}
Finally, pre-formatting the string with Formatter.string(from:) gives the expected result:
import SwiftUI
struct ContentView: View {
var body: some View {
let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
NavigationView {
Text(formatter.string(from: Date())) // "October 27, 2020"
.navigationTitle(
Text(formatter.string(from: Date())) // "October 27, 2020"
)
}
}
}
What's going on here? Do I really need this workaround in order to display a date in the navigation title?
Yes, unfortunately it looks like a bug.
As a workaround you can create an extension:
extension Text {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
init(date: Date) {
self.init(Text.dateFormatter.string(from: date))
}
}
so you have the same amount of code in navigationTitle:
.navigationTitle(
Text(date: Date())
)

Format text field to 2 decimal places without entering a decimal (SwiftUI)

In a text field, I'd like, when a user enters a number e.g. 12345, it gets formatted as 123.45. The user never needs to enter a decimal place, it just uses the 2 right most numbers as the decimal places. The field should only allow numbers too. This is for a SwiftUI project. Thanks in advance for any assistance.
Because there of a two way binding between what you enter and what is being shown in the TextField view it seems not possible to interpolate the displayed number entered. I would suggest a small hack:
create a ZStack with a TextField and a Text View superimposed.
the foreground font of the entered text in the TextField is clear or white .foregroundColor(.clear)
the keyboard is only number without decimal point: .keyboardType(.numberPad)
use .accentColor(.clear) to hide the cursor
the results are displayed in a Text View with formatting specifier: "%.2f"
It would look like
This is the code:
struct ContentView: View {
#State private var enteredNumber = ""
var enteredNumberFormatted: Double {
return (Double(enteredNumber) ?? 0) / 100
}
var body: some View {
Form {
Section {
ZStack(alignment: .leading) {
TextField("", text: $enteredNumber)
.keyboardType(.numberPad).foregroundColor(.clear)
.textFieldStyle(PlainTextFieldStyle())
.disableAutocorrection(true)
.accentColor(.clear)
Text("\(enteredNumberFormatted, specifier: "%.2f")")
}
}
}
}
}
With Swift UI the complete solution is
TextField allow numeric value only
Should accept only one comma (".")
Restrict decimal point upto x decimal place
File NumbersOnlyViewModifier
import Foundation
import SwiftUI
import Combine
struct NumbersOnlyViewModifier: ViewModifier {
#Binding var text: String
var includeDecimal: Bool
var digitAllowedAfterDecimal: Int = 1
func body(content: Content) -> some View {
content
.keyboardType(includeDecimal ? .decimalPad : .numberPad)
.onReceive(Just(text)) { newValue in
var numbers = "0123456789"
let decimalSeparator: String = Locale.current.decimalSeparator ?? "."
if includeDecimal {
numbers += decimalSeparator
}
if newValue.components(separatedBy: decimalSeparator).count-1 > 1 {
let filtered = newValue
self.text = isValid(newValue: String(filtered.dropLast()), decimalSeparator: decimalSeparator)
} else {
let filtered = newValue.filter { numbers.contains($0)}
if filtered != newValue {
self.text = isValid(newValue: filtered, decimalSeparator: decimalSeparator)
} else {
self.text = isValid(newValue: newValue, decimalSeparator: decimalSeparator)
}
}
}
}
private func isValid(newValue: String, decimalSeparator: String) -> String {
guard includeDecimal, !text.isEmpty else { return newValue }
let component = newValue.components(separatedBy: decimalSeparator)
if component.count > 1 {
guard let last = component.last else { return newValue }
if last.count > digitAllowedAfterDecimal {
let filtered = newValue
return String(filtered.dropLast())
}
}
return newValue
}
}
File View+Extenstion
extension View {
func numbersOnly(_ text: Binding<String>, includeDecimal: Bool = false) -> some View {
self.modifier(NumbersOnlyViewModifier(text: text, includeDecimal: includeDecimal))
}
}
File ViewFile
TextField("", text: $value, onEditingChanged: { isEditing in
self.isEditing = isEditing
})
.foregroundColor(Color.neutralGray900)
.numbersOnly($value, includeDecimal: true)
.font(.system(size: Constants.FontSizes.fontSize22))
.multilineTextAlignment(.center)