I have SwiftUI code which computes the time duration between two times (startTime and endTime) and rounds up to nearest 15 minutes. But how do I calculate the currency rate of $220 per hour from this duration?
I also seem to be struggling with organizing my code into view code (for SwiftUI) and also including the numerical code that runs in the background.
But here's my code I have so far with comments where I need to include this code.
import SwiftUI
struct ContentView: View {
#State private var startTime = Date().zeroSeconds
#State private var endTime = Date().zeroSeconds
#State private var number15Intervals = 0
#State private var amountDue = 0.0
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter Case Times:")) {
DatePicker("Start Time", selection: $startTime , displayedComponents: .hourAndMinute)
DatePicker("End Time", selection: $endTime, in: startTime..., displayedComponents: .hourAndMinute)
}
Section(header: Text("Case Duration:")) {
Text("duration = \(self.duration) min")
Text("duration (15m) = \(self.duration15) min")
}
Section(header: Text("Amount Due:")) {
// What code do I put here to calculate currency (US dollars)
// which equals time (rounded up by 15 min) times a rate of $220 per hour?
Text(amountDue, format: .currency(code: Locale.current.currencyCode ?? "USD"))
}
}
.navigationTitle("DDA Rates Calculator")
}
}
var duration: TimeInterval {
guard endTime > startTime else {
return 0
}
let dateIntervalMinutes = DateInterval(start: startTime, end: endTime).duration / 60
return dateIntervalMinutes
}
var duration15: TimeInterval {
return (self.duration/15.0).rounded(.up)*15
}
}
extension Date {
var zeroSeconds: Date {
let calendar = Calendar.current
let dateComponents = calendar.dateComponents([.hour, .minute], from: self)
return calendar.date(from: dateComponents) ?? self
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sorry if I'm not too clear here. I've tried all sorts of ways to do this but always seem to get all sorts of errors from XCode. I think it would be more confusing to show what I've tried so far since I tried it so many ways without success. I'm not understanding the scope and how to reference variables properly in SwiftUI.
You can replace your amountDue #State variable with a computed property:
var amountDue: Double {
duration15 / 60 * 220
}
(You can also remove the unused number15Intervals)
Related
This may just be a limitation of DatePicker in SwiftUI, but I really want to pick my times as input in 24 hour (military) time format. Notice in my app that wheelpicker only gives me the 12-hour format.
I was told you had to change your device settings to 24 hour format for this to work that way, but I did this and this did not change any wheelpicker options.
Anyone else have an idea on how to do this?
Here's my code with start and end times wheelpicker format.
import SwiftUI
struct ContentView: View {
#State private var startTime = Date().zeroSeconds
#State private var endTime = Date().zeroSeconds
private var formatter:DateComponentsFormatter = {
let df = DateComponentsFormatter()
df.allowedUnits = [.hour, .minute]
return df
}()
var amountDue: Double {
let numberIntervals = duration15 / 900
if numberIntervals < 4 {
return 460
} else {
var overTime = numberIntervals - 24
if overTime < 0 {
overTime = 0
}
return 460 + (numberIntervals-4) * 55 + (overTime * 20)
}
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter Case Times:")) {
DatePicker("Start Time", selection: $startTime , displayedComponents: .hourAndMinute)
DatePicker("End Time", selection: $endTime, in: startTime..., displayedComponents: .hourAndMinute)
}
Section(header: Text("Times Selected:")) {
Text("Start Time = \(startTime.formatted(date: .omitted, time: .shortened))")
Text("End Time = \(endTime.formatted(date: .omitted, time: .shortened))")
}
Section(header: Text("Case Duration:")) {
Text("duration = \(self.durationStr)")
Text("duration (rounded) = \(self.duration15Str)")
}
Section(header: Text("Amount Due:")) {
Text(amountDue, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
}
}
.navigationTitle("DDA Rates Calculator")
}
}
var duration: TimeInterval {
guard endTime > startTime else {
return 0
}
return (DateInterval(start: startTime, end: endTime).duration)
}
var duration15: TimeInterval {
return ((self.duration/900.0).rounded(.up)*900)
}
var durationStr: String {
let duration = self.duration
guard duration > 1 else {
return "----"
}
return formatter.string(from: duration) ?? "----"
}
var duration15Str: String {
let duration = duration15
guard duration > 1 else {
return "----"
}
return formatter.string(from: duration) ?? "----"
}
}
extension Date {
var zeroSeconds: Date {
let calendar = Calendar.current
let dateComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: self)
return calendar.date(from: dateComponents) ?? self
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I couldn't find any resources on the web or in Apple Developer documentation that supports what I'm looking for.
Got it to work by changing my device (iPhone) that I downloaded code to. Doesn't work in simulator mode.
Not ideal as I didn't want to change my iPhone to military for just one app. Guess I'd have to either write my own Datepicker or see if Apple updates theirs to have a modifier for this.
I'm currently building out a project where a user could create multiple timers however I'm running into the following error. "Instance method 'onReceive(_:perform:)' requires that 'CountdownTimer' conform to 'Publisher'". I just can't figure out how to get Countdown Timer to conform to Publisher.
Here is the code for my timer object:
struct CountdownTimer: Identifiable {
let id = UUID()
let name: String
var minutes: Int
var seconds: Int
var countdown: Int {
let totalTime = seconds + (minutes * 60)
return totalTime
}
}
func timeString(time: Int) -> String {
return String(format: "%01i:%02i", minutes, seconds)
}
class CountdownTimers: ObservableObject {
#Published var timers = [CountdownTimer]()
}
My Content View:
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var countdownTimers = CountdownTimers()
#State private var showingAddSheet = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
Color.pastelGreen
.edgesIgnoringSafeArea(.all)
VStack {
ScrollView {
ForEach(countdownTimers.timers) { timer in
ZStack {
CardView()
.overlay(
HStack {
VStack(alignment: .leading) {
Text("\(timer.countdown)")
.font(.custom("Quicksand-Bold", size: 30))
Text(" \(timer.timeString(time: timer.countdown))")
.font(.custom("Quicksand-Bold", size: 40))
.onReceive(timer){ _ in
if timers.countdown > 0 {
timers.countdown -= 1
}
}
The error is happening on the .OnReceive(timer) line. the minutes and seconds are created on a different "AddView" but I don't believe that the issue is related to that section.
Any help would be greatly appreciated.
You're shadowing the name timer. It appears first here:
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
Then, later, you use it again here:
ForEach(countdownTimers.timers) { timer in
Swift is going to assume you always mean the timer in the most-recently-defined scope, so when you use .onReceive, it assumes you mean timer from the ForEach (which is of type CountdownTimer) and not the Timer defined earlier.
To fix this, use a different name in your ForEach and adjust your code (such as the Text elements) to use the new name.
I was wondering if someone can help me solve this problem. I have the following rating view:
import SwiftUI
struct RatingView: View {
#Binding var rating: Int
var label = ""
var maxRating = 5
var offImage: Image?
var onImage = Image(systemName: "star.fill")
var offColor = Color.gray
var onColor = Color.yellow
var body: some View {
HStack{
if label.isEmpty == false {
Text(label)
}
ForEach(1..<maxRating + 1, id: \.self){ number in
image(for: number)
.foregroundColor(number > rating ? offColor : onColor)
.onTapGesture {
rating = number
}
}
}
}
func image(for number: Int) -> Image {
if number > rating {
return offImage ?? onImage
} else {
return onImage
}
}
}
struct RatingView_Previews: PreviewProvider {
static var previews: some View {
RatingView(rating: .constant(4))
}
}
I wanted to have a custom rating for each image displayed in the ContenView, which is as follows:
import SwiftUI
struct ContentView: View {
var pictureURL = DbHelper().getPictures()
#EnvironmentObject var authenticator: Authenticator
#State private var rating: Int = 0
var body: some View {
List(pictureURL, id: \.self) { photo in
VStack{
if #available(iOS 15.0, *) {
AsyncImage(url: URL(string: photo), scale: 10.0)
} else {
// Fallback on earlier versions
}
RatingView(rating: $rating)
}
}
Button("Logout") {
authenticator.logout()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Authenticator())
}
}
This code displays the view but when I click on a start, all of the ratings change to that value. I eventually want to add to a database, for specified user who is logged in, the rating, and image URL associated with that rating.
Any hints on how to go about this would be helpful.
Within your loop, all of your rating views bind to the same $rating state variable. So, if you have five images and ratings views for each, if you tap on the first one, that changes ContentView's rating value, and that change is then reflected in the other four.
To have individual ratings, you'll need some way of associating a rating with a picture URL. Depending on how you end up modelling your data, that could involve replacing pictureURL, an array of URL strings, with an array of objects, each of which contains the URL and a bindable rating that applies to that user.
There are other ways of structuring that data, depending on how your final model needs to be structured, but the key will always be that the rating you pass to your RatingView will need to apply to the current URL and no others.
I have an app where I would like the user to be able to start a timed task, and when time runs out, the navigation hierarchy should pop and bring the user back. I have code that ~works, but I don't like the code smell. Is this the right way to approach something like this?
class SimpleTimerManager: ObservableObject {
#Published var elapsedSeconds: Double = 0.0
private(set) var timer = Timer()
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) {_ in
self.elapsedSeconds += 0.01
}
}
func stop() {
timer.invalidate()
elapsedSeconds = 0.0
}
}
struct ContentView: View {
#ObservedObject var timerManager = SimpleTimerManager()
var body: some View {
NavigationView {
NavigationLink(destination: CountDownIntervalView(
timerManager: timerManager, length: 5.0
)) {
Text("Start the timer!")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct CountDownIntervalView: View {
#ObservedObject var timerManager: SimpleTimerManager
var length: Double
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var interval: Double {
let interval = length - self.timerManager.elapsedSeconds
if interval <= 0 {
self.mode.wrappedValue.dismiss()
self.timerManager.stop()
}
return interval
}
var body: some View {
VStack {
Text("Time remaining: \(String(format: "%.2f", interval))")
Button(action: {
self.mode.wrappedValue.dismiss()
self.timerManager.stop()
}) {
Text("Quit early!")
}
}
.navigationBarBackButtonHidden(true)
.onAppear(perform: {
self.timerManager.start()
})
}
}
I have a custom back button, which is something I'd like to preserve. My main concern is that it feels wrong to have code that stops the timer and pops the navigation inside a computed property. I'd prefer something like
Text("\(interval)").onReceive(timerManager.timer, perform: { _ in
if self.interval <= 0 {
self.mode.wrappedValue.dismiss()
self.timerManager.stop()
}
})
inside CountDownIntervalView, but this generates a compiler error - Unable to infer complex closure return type; add explicit type to disambiguate - and, to be honest, I'm not sure that approach makes sense either (attaching the code that conditionally pops the navigation to a piece of UI). What is the "best practices" way of approaching this problem?
Thanks for any thoughts.
Here is a solution. Tested with Xcode 11.4 / iOS 13.4
struct CountDownIntervalView: View {
#ObservedObject var timerManager: SimpleTimerManager
var length: Double
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var interval: Double {
length - self.timerManager.elapsedSeconds
}
var body: some View {
VStack {
Text("Time remaining: \(String(format: "%.2f", interval))")
.onReceive(timerManager.$elapsedSeconds) { _ in
if self.interval <= 0 {
self.mode.wrappedValue.dismiss()
self.timerManager.stop()
}
}
// ... other your code
So I created a line of code that takes two values, a year and a month, and depending on what values you give it, it will calculate the number of days in a given month for a given year. Initially I was going to use State variables so it would just update automatically, but because of how Structs work, I couldn't use the variables I had just barely initialized. (as in the "Cannot use instance member 'year' within property initializer; property initializers run before 'self' is available" Error). The reason I want this code is because I want a forEach Loop to automatically iterate based on that number (as the app I am making will have a list for every day for the next two years). Here is my code:
struct YearView: View {
#State var year = [2020, 2021, 2022]
//monthSymbols gets an array of all the months
#State var monthArray = DateFormatter().monthSymbols!
#State var yearIndex = 0
#State var monthIndex = 0
#State var month = 0
#State var daysInMonth = Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year, month: month + 1))!)!.count
var body: some View {
NavigationView {
List {
Section {
VStack {
Picker("Years", selection: $yearIndex) {
ForEach(0 ..< year.count) { index in
Text(String(self.year[index])).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
Divider()
if yearIndex == 0 {
Picker("Month", selection: $monthIndex) {
ForEach(6 ..< monthArray.count) { index in
Text(self.monthArray[index]).tag(index)
}
}
.padding(.bottom, 2)
} else {
Picker("Month", selection: $monthIndex) {
ForEach(0 ..< monthArray.count) { index in
Text(self.monthArray[index]).tag(index)
}
}
.padding(.bottom, 2)
}
}
}
Section {
ForEach(0..<10) { i in
NavigationLink(destination: ContentView(day: dayData[i])) {
DayRow(day: dayData[i])
}
}
}
}
.navigationBarTitle(Text("\(monthArray[monthIndex + indexTest]) \(String(year[yearIndex]))"))
}
.listStyle(InsetGroupedListStyle())
}
}
The ForEach loop that is by the Navigation Link is the one I want to be iterated. I have tried creating a function as such:
func getRange(year: Int, month: Int) -> Int {
return Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year, month: month + 1))!)!.count
}
I'm not sure where I would run that however, if that would even work. I'm new to SwiftUI and GitHub, so if there's any more info I can give that would help, just ask!
As far as I can tell you should be able to call your getRange(year:, month:) function in your ForEach. Passing in yearIndex and monthIndex.
I wrote up this little sample code to quickly test the theory. Let me know if this is what you're looking for.
-Dan
struct ContentView: View {
#State private var year = 3
#State private var month = 2
var body: some View {
List {
ForEach(0 ..< getRange(year: year, month: month)) { i in
Text("\(i)")
}
}
}
func getRange(year: Int, month: Int) -> Int {
return Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year, month: month + 1))!)!.count
}
}
edit to address the question in your comment;
To update your Picker label properly try changing your code to this:
Picker("Years", selection: $year) {
ForEach(0 ..< year) { index in
Text("\(index)")
}
}
See if that gives you the desired response. It's not what you're doing is entirely wrong, I'm just not super clear on what your ultimate intent is. But play around with the code, I think you're on the right track.