Overall Context
I begin with the below View, which gives me the result image of "Total 0.00" that follows.
My problem: When I enter in values, it either adds the numbers at the beginning or the end of the "0.00" placeholder String in TextField. I can highlight and overwrite the placeholder text of the TextField in the preview with keyboard input.
My goal: I want the input sequence to run as such:
User inputs the first digit of their purchase amount
This first digit replaces the 0 in the 2nd decimal place of the placeholder text in TextField.
As the user input the remaining digits of their purchaser price, the numbers move right to left.
Example: Let's say the user's purchase price was 29.50:
User inputs 2
The TextField changes from 0.00 to 0.02
User inputs 9, the TextField then reads 0.29
This continues until the user finishes inputing 29.50.
TLDR I want the input to run right to left, keeping the decimal in the appropriate place.
import SwiftUI
struct ContentView: View {
#State private var total: String = "0.00"
var body: some View {
NavigationView {
Form {
Section(header: Text("Detail")) {
HStack {
Text("Total")
TextField("0.00", text: $total)
.keyboardType(.decimalPad)
.foregroundColor(.gray)
.frame(width: 120)
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Result of the above ContentView
Attempt to use .onEditingChanged
I replaced the TextField with the below. Trying the below code returns the following error on line the line with .onEditingChanged "Value of type 'some View' has no member 'onEditingChanged'"
I learned .onEditingChanged is not a property of TextField so I needed to try another approach....
TextField("0.00", text: $total)
.keyboardType(.numberPad)
.foregroundColor(.gray)
.frame(width: 120)
.onEditingChanged { value in
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = Locale(identifier: "en_US")
if let result = formatter.string(from: NSNumber(value: Double(value) ?? 0)) {
self.total = result
}
}
Attempt to use .onCommit {}
I replaced the .onEditingChanged { value in from that attempt with .onCommit {. This resulted in the same error message. It read "Value of type 'some View' has no member 'onCommit'"
TextField("0.00", text: $total)
.keyboardType(.decimalPad)
.foregroundColor(.gray)
.frame(width: 120)
.onCommit {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = Locale(identifier: "en_US")
if let result = formatter.string(from: NSNumber(value: Double(self.total) ?? 0)) {
self.total = result
}
}
I am at a loss as how to achieve my goal. Thank you for the help in advance!
Related
this is what i get when i try to change the color sorry can't post image here yet so i suggested to use link
I used RectangleMark from apple Charts Api
here is my chart, my chartData has low, high, open, and close prices
I want to have it to look like a normal candle stick chart, red when it is down, green when it is up using the open and close price value
I made a #State chartColor to have it changed with func to trigger it, but I get all of the chart red instead of a proper candle stick chart
Here is my code
import SwiftUI
import Charts
struct CoinChart: View {
#ObservedObject var chartManager: ChartManager
#State var chartColor: Color
func colorChange(){
for content in chartManager.chartData.data {
let openPrice = Double(content.open)
let closePrice = Double(content.close)
if closePrice! < openPrice! {
chartColor = Color.red
} else {
chartColor = Color.green
}
}
}
var body: some View {
VStack {
Chart{
ForEach(chartManager.chartData.data, id: \.self) { chartData in
let date = chartManager.unixConverter(unixTime: Double(chartData.period))
let dates = Date(timeIntervalSince1970: Double(chartData.period))
let lowPrice = Double(chartData.low)
let highPrice = Double(chartData.high)
let openPrice = Double(chartData.open)
let closePrice = Double(chartData.close)
RectangleMark(x: .value("Time", date),
yStart: .value("Low Price", lowPrice!),
yEnd: .value("High Price", highPrice!), width: 1)
.foregroundStyle(chartColor)
RectangleMark(x: .value("Time", date),
yStart: .value("Open Price", openPrice!),
yEnd: .value("Close Price", closePrice!), width: 6)
.foregroundStyle(chartColor)
}
}
.chartYScale(domain: 18000...21000)
.chartXAxis {
AxisMarks(position: .bottom) { _ in
AxisGridLine().foregroundStyle(.clear)
AxisTick().foregroundStyle(.clear)
AxisValueLabel()
}
}
.frame(height: 300)
Text(String(chartManager.chartData.data.count))
}
.task {
await chartManager.loadChartData()
colorChange()
}
}
}
I called the func colorChange() in the task cuz if I call it in the chart it will give me a purple warning
Modifying state during view update, this will cause undefined behavior.
Any help will be appreciated everyone?
Thanks in advance
So i need to make the decimal places smaller than the actual number i will show you what i mean below.
this is the code i have:
import SwiftUI
struct BalanceDetailsView: View {
//MARK: - PROPERTIES
var balance: Float
var balanceString: String {
return balance.formattedWithSeparator
}
//MARK: - BODY
var body: some View {
VStack{
HStack{
Text(balanceString)
.font(.custom(K.fonts.gilroyBold, size: 24))
.multilineTextAlignment(.center)
.padding(.top)
}//:VSTACK
}//:HSTACK
}
}
struct BalanceDetailsView_Previews: PreviewProvider {
static var previews: some View {
BalanceDetailsView(balance: 43678)
.previewLayout(.sizeThatFits)
}
}
//Formatter extension i used to get this code
extension Formatter {
static let withSeparator: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
// minimum decimal digit, eg: to display 2 as 2.00
formatter.minimumFractionDigits = 2
// maximum decimal digit, eg: to display 2.5021 as 2.50
formatter.maximumFractionDigits = 2
return formatter
}()
}
extension Numeric {
var formattedWithSeparator: String { Formatter.withSeparator.string(for: self) ?? "" }
}
Result I get
Result I need
When you know exact format of your string, like in this case minimum string length will be 4("0.00") , you can safely use dropLast and dropFirst.
I suggest moving 2 to priceFractionDigits constant to reduce constants usage in your code.
Then you can use string concatenation, it'll align Text by baseline.
struct BalanceText: View {
var balance: Float
var balanceString: String {
return balance.formattedWithSeparator
}
var body: some View {
Text(balanceString.dropLast(priceFractionDigits))
.font(.system(size: 24))
+
Text(balanceString.dropFirst(balanceString.count - priceFractionDigits))
.font(.system(size: 18))
}
}
private let priceFractionDigits = 2
extension Formatter {
static let withSeparator: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
// minimum decimal digit, eg: to display 2 as 2.00
formatter.minimumFractionDigits = priceFractionDigits
// maximum decimal digit, eg: to display 2.5021 as 2.50
formatter.maximumFractionDigits = priceFractionDigits
return formatter
}()
}
Usage
BalanceText(balance: balance)
Here is an example using the new AttributedString in SwiftUI 3 (iOS 15, macOS 12 etc)
var balanceString: AttributedString {
var attributedString = AttributedString(balance.formattedWithSeparator)
guard let separator = Formatter.withSeparator.decimalSeparator else { return attributedString }
if let range = attributedString.range(of: separator) {
attributedString[attributedString.startIndex...attributedString.index(beforeCharacter: range.lowerBound)]
.font = Font.largeTitle
attributedString[attributedString.index(afterCharacter: range.lowerBound)..<attributedString.endIndex]
.font = Font.caption
}
return attributedString
}
I used some built in font styles here but that should be easy to replace. Also note that since we set the .font attribute here it should be removed from Text
You can concatenate Text together with the + operator.
I slightly change the way you use NumberFormatter, so it's split by the correct decimal character for the locale.
struct BalanceDetailsView: View {
private let wholeNumberPart: String
private let decimalNumberPart: String
init(balance: Double) {
(wholeNumberPart, decimalNumberPart) = balance.formattedSplittingBySeparator
}
var body: some View {
Text(wholeNumberPart).font(.system(size: 24)) +
Text(decimalNumberPart).font(.system(size: 16))
}
}
extension Numeric {
var formattedSplittingBySeparator: (whole: String, decimal: String) {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
let str = formatter.string(for: self) ?? "0\(formatter.decimalSeparator!)00"
let split = str.components(separatedBy: formatter.decimalSeparator)
let whole = (split.first ?? "0") + formatter.decimalSeparator
let decimal = split.count == 2 ? split[1] : "00"
return (whole: whole, decimal: decimal)
}
}
Usage:
BalanceDetailsView(balance: 43678)
Result:
You could split the string by the decimal separator and display the parts in a zero-spacing HStack.
struct BalanceView : View {
let balance : Float
var body: some View {
let components = balance
.formattedWithSeparator
.components(separatedBy: Formatter.withSeparator.decimalSeparator)
HStack(alignment: .firstTextBaseline, spacing: 0) {
Text(components[0])
if components.count > 1 {
Text(".")
Text(components[1])
.font(.title)
}
}
.font(.largeTitle)
.multilineTextAlignment(.center)
.padding(.top)
}
}
Replace the fonts with your custom fonts
In your environment you could use it
struct BalanceDetailsView: View {
//MARK: - PROPERTIES
var balance: Float
//MARK: - BODY
var body: some View {
VStack{
BalanceView(balance: balance)
}//:VSTACK
}
}
It is possible to pass a date to Text() in SwiftUI, then format it as a timer using the style argument. However, a countdown like this never stops, it just keeps incrementing after zero. How to make it stop at 0?
func nextRollTime(in seconds: Int) -> Date {
let date = Calendar.current.date(byAdding: .second, value: seconds, to: Date())
return date ?? Date()
}
Above is the function I use to start a countdown, then I pass it as follows:
Text(nextRollTime(in: 20), style: .timer)
Here is a demo of possible approach - as .timer run from now for ever (by design), the idea is to replace it with regular text once specified period is over.
Tested with Xcode 12b3 / iOS 14.
struct DemoView: View {
#State private var run = false
var body: some View {
VStack {
if run {
Text(nextRollTime(in: 10), style: .timer)
} else {
Text("0:00")
}
}
.font(Font.system(.title, design: .monospaced))
.onAppear {
self.run = true
}
}
func nextRollTime(in seconds: Int) -> Date {
let date = Calendar.current.date(byAdding: .second, value: seconds, to: Date())
DispatchQueue.main.asyncAfter(deadline: .now() + Double(seconds)) {
self.run = false
}
return date ?? Date()
}
}
I'm building an app that offers a service with something similar to dog walking. The people who will walk the dogs can upload the days and times they are available. Instead of them picking an actual date like Mon Jan 1st I let them just pick whatever days of the week and whatever times they are avail.
The problem I'm having is I can't figure out how to construct a data model for it.
What's in the photo is a collectionView with a cell and in each cell I show the available day and times slots that they can pick. Each day of the week has the same 7 time slots that a user who wants to be the dog walker can pick from.
The thing is if someone picks Sun 6am-9am, 12pm-3pm, and 6pm-9pm but they also pick Mon 6am-9m, how can I construct a data model that can differentiate between the days and times. For eg Sunday at 6am - 9am and Mon 6am-9am, how to tell the difference? Should those time slots be Doubles or Strings?
This is what I'm currently using for the collectionView data source and the cell:
// the collectionView's data source
var tableData = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
//cellForItem
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: availabilityCell, for: indexPath) as! AvailabilityCell
cell.clearCellForReuse()
cell.dayOfWeek = tableData[indexPath.item]
// inside the AvailabilityCell itself
var dayOfWeek: String? {
didSet {
dayOfWeekLabel.text = dayOfWeek
}
}
func clearCellForReuse() {
dayOfWeekLabel.text = nil
// deselect whatever radio buttons were selected to prevent scrolling issues
}
For a little further explanation what will eventually happen is when the user who wants their dog walks scrolls through to see who is avail, if the day and time their scrolling isn't on any of the days and times the person who posted (Sun & Mon with the chosen hours) isn't available, then their post shouldn't appear in the feed but if it is one of those days and one of those hours then their post will appear in the feed (in the example if someone is scrolling on Sunday at 10pm this post shouldn't appear). Whatever is in the data model will get compared to whatever day and time the posts are currently getting scrolled . I'm using Firebase for the backend.
What I came up with is rather convoluted and that's why I need something more reasonable.
class Availability {
var monday: String?
var tuesday: String?
var wednesday: String?
var thursday: String?
var friday: String?
var saturday: String?
var sunday: String?
var slotOne: Double? // sunday 6am-9am I was thinking about putting military hours here that's why I used a double
var slotTwo: Double? // sunday 9am-12pm
var slotTwo: Double? // sunday 12pm-3pm
// these slots would continue all through saturday and this doesn't seem like the correct way to do this. There would be 49 slots in total (7 days of the week * 7 different slots per day)
}
I also thought about maybe separating them into different data models like a Monday class, Tuesday class etc but that didn't seem to work either because they all have to be the same data type for the collectionView datasource.
UPDATE
In #rob's answer he gave me some insight to make some changes to my code. I'm still digesting it but I still have a a couple of problems. He made a cool project that shows his idea.
1- Since I’m saving the data to Firebase database, how should the data get structured to get saved? There can be multiple days with similar times.
2- I'm still wrapping my head around rob's code because I've never dealt with time ranges before so this is foreign to me. I'm still lost with what to sort against especially the time ranges against inside the callback
// someone is looking for a dog walker on Sunday at 10pm so the initial user who posted their post shouldn't appear in the feed
let postsRef = Database().database.reference().child("posts")
postsRef.observe( .value, with: { (snapshot) in
guard let availabilityDict = snapshot.value as? [String: Any] else { return }
let availability = Availability(dictionary: availabilityDict)
let currentDayOfWeek = dayOfTheWeek()
// using rob;s code this compares the days and it 100% works
if currentDayOfWeek != availability.dayOfWeek.text {
// don't add this post to the array
return
}
let currentTime = Calendar.current.dateComponents([.hour,.minute,.second], from: Date())
// how to compare the time slots to the current time?
if currentTime != availability.??? {
// don't add this post to the array
return
}
// if it makes this far then the day and the time slots match up to append it to the array to get scrolled
})
func dayOfTheWeek() -> String? {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "EEEE"
return dateFormatter.stringFromDate(self)
}
There are lots of ways to skin the cat, but I might define the availability as day enumeration and time range:
struct Availability {
let dayOfWeek: DayOfWeek
let timeRange: TimeRange
}
Your day of the week might be:
enum DayOfWeek: String, CaseIterable {
case sunday, monday, tuesday, wednesday, thursday, friday, saturday
}
Or you could also do:
enum DayOfWeek: Int, CaseIterable {
case sunday = 0, monday, tuesday, wednesday, thursday, friday, saturday
}
Their are pros and cons of both Int and String. The string representation is easier to read in the Firestore web-based UI. The integer representation offers easier sorting potential.
Your time range:
typealias Time = Double
typealias TimeRange = Range<Time>
extension TimeRange {
static let allCases: [TimeRange] = [
6 ..< 9,
9 ..< 12,
12 ..< 15,
15 ..< 18,
18 ..< 21,
21 ..< 24,
24 ..< 30
]
}
In terms of interacting with Firebase, it doesn’t understand enumerations and ranges, so I’d define an init method and dictionary property to map to and from [String: Any] dictionaries that you can exchange with Firebase:
struct Availability {
let dayOfWeek: DayOfWeek
let timeRange: TimeRange
init(dayOfWeek: DayOfWeek, timeRange: TimeRange) {
self.dayOfWeek = dayOfWeek
self.timeRange = timeRange
}
init?(dictionary: [String: Any]) {
guard
let dayOfWeekRaw = dictionary["dayOfWeek"] as? DayOfWeek.RawValue,
let dayOfWeek = DayOfWeek(rawValue: dayOfWeekRaw),
let startTime = dictionary["startTime"] as? Double,
let endTime = dictionary["endTime"] as? Double
else {
return nil
}
self.dayOfWeek = dayOfWeek
self.timeRange = startTime ..< endTime
}
var dictionary: [String: Any] {
return [
"dayOfWeek": dayOfWeek.rawValue,
"startTime": timeRange.lowerBound,
"endTime": timeRange.upperBound
]
}
}
You could also define a few extensions to make this easier to work with, e.g.,
extension Availability {
func overlaps(_ availability: Availability) -> Bool {
return dayOfWeek == availability.dayOfWeek && timeRange.overlaps(availability.timeRange)
}
}
extension TimeRange {
private func string(forHour hour: Int) -> String {
switch hour % 24 {
case 0: return NSLocalizedString("Midnight", comment: "Hour text")
case 1...11: return "\(hour % 12)" + NSLocalizedString("am", comment: "Hour text")
case 12: return NSLocalizedString("Noon", comment: "Hour text")
default: return "\(hour % 12)" + NSLocalizedString("pm", comment: "Hour text")
}
}
var text: String {
return string(forHour: Int(lowerBound)) + "-" + string(forHour: Int(upperBound))
}
}
extension DayOfWeek {
var text: String {
switch self {
case .sunday: return NSLocalizedString("Sunday", comment: "DayOfWeek text")
case .monday: return NSLocalizedString("Monday", comment: "DayOfWeek text")
case .tuesday: return NSLocalizedString("Tuesday", comment: "DayOfWeek text")
case .wednesday: return NSLocalizedString("Wednesday", comment: "DayOfWeek text")
case .thursday: return NSLocalizedString("Thursday", comment: "DayOfWeek text")
case .friday: return NSLocalizedString("Friday", comment: "DayOfWeek text")
case .saturday: return NSLocalizedString("Saturday", comment: "DayOfWeek text")
}
}
}
If you don’t want to use Range, you can just define TimeRange as a struct:
enum DayOfWeek: String, CaseIterable {
case sunday, monday, tuesday, wednesday, thursday, friday, saturday
}
extension DayOfWeek {
var text: String {
switch self {
case .sunday: return NSLocalizedString("Sunday", comment: "DayOfWeek text")
case .monday: return NSLocalizedString("Monday", comment: "DayOfWeek text")
case .tuesday: return NSLocalizedString("Tuesday", comment: "DayOfWeek text")
case .wednesday: return NSLocalizedString("Wednesday", comment: "DayOfWeek text")
case .thursday: return NSLocalizedString("Thursday", comment: "DayOfWeek text")
case .friday: return NSLocalizedString("Friday", comment: "DayOfWeek text")
case .saturday: return NSLocalizedString("Saturday", comment: "DayOfWeek text")
}
}
}
struct TimeRange {
typealias Time = Double
let startTime: Time
let endTime: Time
}
extension TimeRange {
static let allCases: [TimeRange] = [
TimeRange(startTime: 6, endTime: 9),
TimeRange(startTime: 9, endTime: 12),
TimeRange(startTime: 12, endTime: 15),
TimeRange(startTime: 15, endTime: 18),
TimeRange(startTime: 18, endTime: 21),
TimeRange(startTime: 21, endTime: 24),
TimeRange(startTime: 24, endTime: 30)
]
func overlaps(_ availability: TimeRange) -> Bool {
return (startTime ..< endTime).overlaps(availability.startTime ..< availability.endTime)
}
}
extension TimeRange {
private func string(forHour hour: Int) -> String {
switch hour % 24 {
case 0: return NSLocalizedString("Midnight", comment: "Hour text")
case 1...11: return "\(hour % 12)" + NSLocalizedString("am", comment: "Hour text")
case 12: return NSLocalizedString("Noon", comment: "Hour text")
default: return "\(hour % 12)" + NSLocalizedString("pm", comment: "Hour text")
}
}
var text: String {
return string(forHour: Int(startTime)) + "-" + string(forHour: Int(endTime))
}
}
struct Availability {
let dayOfWeek: DayOfWeek
let timeRange: TimeRange
init(dayOfWeek: DayOfWeek, timeRange: TimeRange) {
self.dayOfWeek = dayOfWeek
self.timeRange = timeRange
}
init?(dictionary: [String: Any]) {
guard
let dayOfWeekRaw = dictionary["dayOfWeek"] as? DayOfWeek.RawValue,
let dayOfWeek = DayOfWeek(rawValue: dayOfWeekRaw),
let startTime = dictionary["startTime"] as? Double,
let endTime = dictionary["endTime"] as? Double
else {
return nil
}
self.dayOfWeek = dayOfWeek
self.timeRange = TimeRange(startTime: startTime, endTime: endTime)
}
var dictionary: [String: Any] {
return [
"dayOfWeek": dayOfWeek.rawValue,
"startTime": timeRange.startTime,
"endTime": timeRange.endTime
]
}
}
extension Availability {
func overlaps(_ availability: Availability) -> Bool {
return dayOfWeek == availability.dayOfWeek && timeRange.overlaps(availability.timeRange)
}
}
I have been playing with MeasurementFormatter to try and display imperial lengths as 10'6" or 10 ft 6 in unsuccessfully. LengthFormatter does this correctly when isForPersonHeightUse is set to true, but it does not adapt to the user's locale well (i.e. countries where length is measured in metric, except for when referring to height). Is there any way to force this behaviour in a formatter?
EDIT
This question is to determine how to choose the units for a measurement. I am able to choose feet or inches, but want to display fractional feet as inches as in: 6'3" instead of 6.25 ft.
public struct LengthFormatters {
public static let imperialLengthFormatter: LengthFormatter = {
let formatter = LengthFormatter()
formatter.isForPersonHeightUse = true
return formatter
}()
}
extension Measurement where UnitType : UnitLength {
var heightOnFeetsAndInches: String? {
guard let measurement = self as? Measurement<UnitLength> else {
return nil
}
let meters = measurement.converted(to: .meters).value
LengthFormatters.imperialLengthFormatter.string(fromMeters: meters)
}
}
Example of using:
let a = Measurement(value: 6.34, unit: UnitLength.feet)
print(a.heightOnFeetsAndInches ?? "")
let b = Measurement(value: 1.5, unit: UnitLength.feet)
print(b.heightOnFeetsAndInches ?? "")
Will print:
6 ft, 4.08 in
1 ft, 6 in
I modified (simplified) #maslovsa's answer to meet my needs. I have a Core Data object called "Patient". It has a height parameter in inches that is an Int64. I want a string that I display to the user, so here's my property on my patient object for doing so:
var heightInFeetString : String {
let measurement = Measurement(value: Double(self.height) / 12.0, unit: UnitLength.feet)
let meters = measurement.converted(to: .meters).value
return LengthFormatter.imperialLengthFormatter.string(fromMeters: meters)
}
Of course, I had to implement the imperialLengthFormatter as well, but I did it as an extension to LengthFormatter itself, like this:
extension LengthFormatter {
public static let imperialLengthFormatter: LengthFormatter = {
let formatter = LengthFormatter()
formatter.isForPersonHeightUse = true
return formatter
}()
}
This actually doesn't kill performance as suggested in the comments for #maslova's answer. Due to the property being static, it only gets initialized once.
// When creating the Patient object
let patient = Patient(...) // Create in maanged object context
patient.height = 71
// Later displays in a collection view cell in a view controller
cell.heightLabel.Text = patient.heightInFeetString
Displays this in my table cell:
5 ft, 11 in
How to display Feet and Inches in SwiftUI
In case anyone arrives here looking for a SwiftUI answer.
struct MeasurementTestView: View {
#State private var height = Measurement(value: 68, unit: UnitLength.inches)
var body: some View {
VStack {
Text(height, format: .measurement(width: .narrow, usage: .personHeight))
Text(height, format: .measurement(width: .abbreviated, usage: .personHeight))
Text(height, format: .measurement(width: .wide, usage: .personHeight))
}
.font(.title)
}
}
Result