I am using a simple view to display text in NumberFormatter for a specific locale
struct CurrencyView: View {
let currency:Currency
var body: some View {
Text(currency.getFormattedCurrency())
}
}
struct CurrencyView_Previews: PreviewProvider {
static var previews: some View {
CurrencyView(currency: Currency(currencyValue: 1.0)).previewLayout(.fixed(width: 300.0, height: 55.0)).environment(\.locale, .init(identifier: "fr_Fr"))
}
}
struct Currency{
let currencyValue:Double
func getFormattedCurrency() -> String{
let formatter = NumberFormatter()
formatter.numberStyle = .currency
let formatterCurrency = formatter.string(for: self.currencyValue) ?? ""
return formatterCurrency
}
}
I was expecting the preview will show currency in French with € as I have already mentioned in Locale in preview.
Here is a solution. Tested with Xcode 11.4
struct CurrencyView: View {
#Environment(\.locale) var locale
let currency:Currency
var body: some View {
Text(currency.getFormattedCurrency(for: locale))
}
}
struct CurrencyView_Previews: PreviewProvider {
static var previews: some View {
CurrencyView(currency: Currency(currencyValue: 1.0))
.environment(\.locale, .init(identifier: "fr_Fr"))
.previewLayout(.fixed(width: 300.0, height: 55.0))
}
}
struct Currency{
let currencyValue:Double
func getFormattedCurrency(for locale: Locale) -> String{
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = locale
let formatterCurrency = formatter.string(for: self.currencyValue) ?? ""
return formatterCurrency
}
}
Related
I have an array of reminders(reminder model) in a view model and want to be able to edit existing reminders specifically through and edit swipe action and then through the reminders detail screen. I tried adding a button with a sheet to my Homeview in a list and then tried updating the edited reminder in the reminders array to a property in my view model called existingRemindData by using an update function in the reminder model. this should work but the remind var created by the foreach loop in the home view doesn't keep its value when it is called in the sheet. In the home view under the edit swipe action when I assign homevm.existingRemindData = remind.data it is equal to whatever reminder I swipe on because I did a print statement to confirm but as soon as I try to use the remind var inside of the sheet for the edit action the remind var defaults to the first item in the reminder array in the view model which is obviously not right. how would I make it so it uses the correct reminder index value when trying to update the reminder or is there another way which I could implement this functionality. any help would be great and look in the code for clarification on what I talk about.
HomeView
'''
import SwiftUI
struct HomeView: View {
#StateObject private var homeVM = HomeViewModel()
#State var percent: Int = 1
#State var showDetailEditView = false
#State var showAddView = false
#State var dropDown = false
//#State var filter = false
var body: some View {
ZStack {
VStack {
List {
ForEach($homeVM.reminds) { $remind in
ReminderView(remind: $remind)
//.background(remind.theme.mainColor)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.swipeActions(edge: .leading) {
Button(action: {
self.showDetailEditView.toggle()
homeVM.existingRemindData = remind.data
print(homeVM.existingRemindData.title)
}) {
Label("Edit", systemImage: "pencil")
}
}
.sheet(isPresented: $showDetailEditView) {
NavigationView {
ReminderEditView(data: $homeVM.existingRemindData)
.navigationTitle(homeVM.existingRemindData.title)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
self.showDetailEditView.toggle()
homeVM.existingRemindData = Reminder.Data()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
self.showDetailEditView.toggle()
print("\(remind.id) \(remind.title)")
print("\(homeVM.existingRemindData.id) \(homeVM.existingRemindData.title)")
remind.update(from: homeVM.existingRemindData)
homeVM.newRemindData = Reminder.Data()
}
}
}
.background(LinearGradient(gradient: Gradient(colors: [
Color(UIColor(red: 0.376, green: 0.627, blue: 0.420, alpha: 1)),
Color(UIColor(red: 0.722, green: 0.808, blue: 0.725, alpha: 1))
]), startPoint: .topLeading, endPoint: .bottomTrailing))
}
}
.swipeActions(allowsFullSwipe: true) {
Button (role: .destructive, action: {
homeVM.deleteReminder(remind: remind)
}) {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
.onAppear(
perform: {
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
})
'''
Reminder edit view
'''
import SwiftUI
extension Binding {
static func ??(lhs: Binding<Optional<Value>>, rhs: Value) -> Binding<Value> {
return Binding(get: {lhs.wrappedValue ?? rhs}, set: {lhs.wrappedValue = $0})
}
}
struct ReminderEditView: View {
#ObservedObject var editVM: EditViewModel
init(data: Binding<Reminder.Data>) {
editVM = EditViewModel(data: data)
}
var body: some View {
Form {
Section {
TextField("Title", text: $editVM.data.title)
TextField("Notes", text: $editVM.data.notes ?? "")
.frame(height: 100, alignment: .top)
}
Section {
Toggle(isOn: $editVM.data.hasDueDate, label: {
if editVM.data.hasDueDate {
VStack(alignment: .leading) {
Text("Date")
Text(editVM.data.hasDueDate ? editVM.data.formatDate(date: editVM.data.date!) : "\(editVM.data.formatDate(date: Date.now))")
.font(.caption)
.foregroundColor(.red)
}
} else {
Text("Date")
}
})
if editVM.data.hasDueDate {
DatePicker("Date", selection: $editVM.data.dueDate, in: Date()..., displayedComponents: .date)
.datePickerStyle(.graphical)
}
'''
Reminder model
'''
extension Reminder {
struct Data: Identifiable {
var title: String = ""
var notes: String?
var date: Date?
var time: Date?
var theme: Theme = .poppy
var iscomplete: Bool = false
var priority: RemindPriority = .None
let id: UUID = UUID()
var dueDate: Date {
get {
return date ?? Date()
}
set {
date = newValue
}
}
var dueTime: Date {
get {
return time ?? Date()
}
set {
time = newValue
}
}
func formatDate(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .none
return formatter.string(from: date)
}
func formatTime(time: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter.string(from: time)
}
var hasDueDate: Bool {
get {
date != nil
}
set {
if newValue == true {
date = Date()
}
else {
date = nil
hasDueTime = false
}
}
}
var hasDueTime: Bool {
get {
time != nil
}
set {
if newValue == true {
time = Date()
hasDueDate = true
}
else {
time = nil
}
}
}
}
var data: Data {
Data(title: title, notes: notes, date: date, time: time, theme: theme, iscomplete: iscomplete, priority: priority)
}
mutating func update(from data: Data) {
title = data.title
notes = data.notes
date = data.date
time = data.time
theme = data.theme
iscomplete = data.iscomplete
priority = data.priority
}
init(data: Data) {
title = data.title
notes = data.notes
date = data.date
time = data.time
theme = data.theme
iscomplete = data.iscomplete
priority = data.priority
id = data.id
}
}
'''
HomeViewModel(View model talked about)
'''
import Foundation
import SwiftUI
import Combine
class HomeViewModel: ObservableObject {
#Published var reminds: [Reminder] = Reminder.sampleReminders
#Published var newRemindData = Reminder.Data()
#Published var existingRemindData = Reminder.Data()
#Published var selectedRemind = Reminder(data: Reminder.Data())
#Published var compReminds: [Reminder] = []
private var cancellables = Set<AnyCancellable>()
/*init(reminds: [Reminder]) {
self.reminds = reminds
}*/
func newReminder() {
let newRemind = Reminder(data: newRemindData)
reminds.append(newRemind)
newRemindData = Reminder.Data()
}
func deleteReminder(remind: Reminder) {
Just(remind)
.delay(for: .seconds(0.25), scheduler: RunLoop.main)
.sink {remind in
if remind.iscomplete {
self.removeRemind(remind: remind)
}
if !remind.iscomplete {
self.removeRemind(remind: remind)
}
self.reminds.removeAll { $0.id == remind.id }
}
.store(in: &cancellables)
}
func appendRemind(complete: Reminder) {
compReminds.append(complete)
}
func removeRemind(remind: Reminder) {
compReminds.removeAll() { $0.id == remind.id }
}
func remindIndex() -> Int {
return reminds.firstIndex(where: {
$0.id == existingRemindData.id
}) ?? 1
}
We don't use view model objects in SwiftUI. Change EditViewModel class to be an EditConfig struct, declare it as #State var config: EditConfig? use it as the item in sheet(item:onDismiss:content:) instead of the bool version.
Also, your date formatting is not SwiftUI compatible, you won't benefit from the labels being updated automatically when the user changes their region settings. To fix that remove the formatDate code and instead supply the formatter to Text or simply use .date. If using a formatter object make sure you aren't initing a new one every time, e.g. store one inside an #State struct or a static var. in SwiftUI we must not init objects in a View's init and body, only value types.
I want to set DatePicker from a String to a certain date when it appears.
#State var staff: Staff
DatePicker(selection: $staff.birthDate, in: ...Date(),displayedComponents: .date) {
Text("Birth Date:")
}
What I expect is something like that, but it didn't work because selection requires Binding<Date> and $staff.birthDate is a String. How do I convert it?
I tried to create a func to format it like so:
func formatStringToDate(date: String?) -> Binding<Date> {
#Binding var bindingDate: Date
let dateForm = DateFormatter()
dateForm.dateFormat = "dd-MM-YYYY"
// _bindingDate = date
return dateForm.date(from: date ?? "")
}
But it still doesn't work. Any idea on how to solve this? Or did I approach this wrong?
Thankyou in advance.
You need to use another date parameter for birth date.
So your Staff class is
class Staff: ObservableObject {
#Published var birthDate: String = "02-05-2021"
#Published var date: Date = Date()
init() {
self.date = DateFormatter.formate.date(from: birthDate) ?? Date()
}
}
Now create one date formattter in the DateFormatter extension.
extension DateFormatter {
static var formate: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd-MM-yyyy"
return dateFormatter
}
}
Now, in view struct use the .onChange method. It will execute each time when the date is changed and you need to convert this date to a string and store it in your birthDate string var.
struct ContentView: View {
#StateObject var staff: Staff = Staff()
var body: some View {
DatePicker(selection: $staff.date, in: ...Date(),displayedComponents: .date) {
Text("Birth Date:")
}
.onChange(of: staff.date) { (date) in
staff.birthDate = DateFormatter.formate.string(from: date)
}
}
}
If your project target is from iOS 13 then you can use custom binding.
struct ContentView: View {
#StateObject var staff: Staff = Staff()
var body: some View {
DatePicker(selection: Binding(get: {staff.date},
set: {
staff.date = $0;
staff.birthDate = DateFormatter.formate.string(from: $0)
}), in: ...Date(),displayedComponents: .date) {
Text("Birth Date:")
}
}
}
Im a total noob to Swift and SwiftUI and im trying to build a project for myself where i can track my workouts and learn myself some Swift.
The problem im hitting is that i have a view which shows all my workout sessions and are formatted by a setting i have in UserDefaults. The user can change this setting to 'metric' or 'imperial'
When changing this the view should update to represent those changes.
The data i have is:
extension WorkoutSession {
#nonobjc public class func fetchRequest() -> NSFetchRequest<WorkoutSession> {
return NSFetchRequest<WorkoutSession>(entityName: "WorkoutSession")
}
#NSManaged public var created_at: Date?
#NSManaged public var id: UUID?
#NSManaged public var reps: Int16
#NSManaged public var sets: Int16
#NSManaged public var updated_at: Date?
#NSManaged public var weight: Double
#NSManaged public var height: Double
#NSManaged public var notes: String?
#NSManaged public var type: Workout?
override public func awakeFromInsert() {
super.awakeFromInsert()
setPrimitiveValue(UUID(), forKey: "id")
setPrimitiveValue(Date(), forKey: "updated_at")
}
override public func willSave() {
super.willSave()
if let updated_at = updated_at {
if updated_at.timeIntervalSince(Date()) > 10.0 {
self.updated_at = Date()
}
} else {
self.updated_at = Date()
}
}
var formattedWeight: String {
var measurement = Measurement(value: self.weight, unit: UserDefaultsWrapper().savedUnitWeight)
let numberFormatter = NumberFormatter()
let measurementFormatter = MeasurementFormatter()
measurement.convert(to:UserDefaultsWrapper().getWeightUnit)
numberFormatter.maximumFractionDigits = 2
measurementFormatter.unitOptions = .providedUnit
measurementFormatter.numberFormatter = numberFormatter
return measurementFormatter.string(from: measurement)
}
// No matter the user defined weight we always save the values in Kilogram so we can convert to anything afterwards
func convertWeightFromUserDefinedUnitToKilogram(weight: Double) -> Double {
let measurement = Measurement(value: weight, unit: UserDefaultsWrapper().getWeightUnit)
return measurement.converted(to: UserDefaultsWrapper().savedUnitWeight).value
}
}
extension WorkoutSession : Identifiable {
}
I also made myself a helper for userDefaults
import SwiftUI
import Foundation
enum UserDefaultsKeys: String {
case measurementUnit = "measurementUnit"
}
enum measurementUnit: String, CaseIterable {
case metric = "metric"
case imperial = "Imperial"
}
struct UserDefaultsWrapper {
let defaults = UserDefaults.standard
var savedUnitWeight = UnitMass.kilograms
var savedUnitHeight = UnitLength.meters
#AppStorage(UserDefaultsKeys.measurementUnit.rawValue) var selectedUnit: String = measurementUnit.metric.rawValue
var getMeasurementUnit: measurementUnit {
get {
let locale = Locale.current
let systemMeasurementUnit = locale.usesMetricSystem ? measurementUnit.metric : measurementUnit.imperial
return measurementUnit(rawValue: selectedUnit) ?? systemMeasurementUnit
}
set(unit) {
defaults.set(unit.rawValue, forKey: UserDefaultsKeys.measurementUnit.rawValue)
}
}
var getWeightUnit: UnitMass {
switch getMeasurementUnit {
case measurementUnit.imperial:
return UnitMass.pounds
case measurementUnit.metric:
fallthrough
default:
return UnitMass.kilograms
}
}
var getHeightUnit: UnitLength {
switch getMeasurementUnit {
case measurementUnit.imperial:
return UnitLength.feet
case measurementUnit.metric:
fallthrough
default:
return UnitLength.meters
}
}
}
And this is the view
import SwiftUI
struct WorkoutDetailView: View {
#Environment(\.managedObjectContext) var moc
#State private var showingAddWorkoutView = false
#ObservedObject var workout: Workout
var body: some View {
List {
ForEach(workout.sessionsArray, id: \.id) { session in
Text("\(session.formattedWeight)")
}
.onDelete(
perform: { offsets in
self.removeItems(at: offsets, from: workout)
}
)
}
.listStyle(InsetGroupedListStyle())
.navigationTitle(workout.name ?? "Unknown Workout")
.navigationBarItems(
leading: EditButton(),
trailing:
Button(action: {
self.showingAddWorkoutView = true
}) {
Image(systemName: "plus")
}
)
.sheet(isPresented: $showingAddWorkoutView) {
AddWorkoutSession(workout: workout)
.environment(\.managedObjectContext, moc)
}
}
func removeItems(at offsets: IndexSet, from workout: Workout) {
for offset in offsets {
let sessionToDelete = workout.sessionsArray[offset]
workout.removeFromSessions(sessionToDelete)
moc.delete(sessionToDelete)
}
if moc.hasChanges{
try? moc.save()
}
}
}
The problem is that when i change the unit from imperial to metric and i go back to the session view the view is not updated, but when i go back to the workout view and open the session view again the changes are there.
Please let me know if you need more code.
I'm building an iOS app with SwiftUI. When I click the "done" button, and the entry property is not nil, and I have not used the DatePicker TextField or TextView, I get the following runtime error in AppDelegate:
Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee2a83fe8)
Here is my code:
import SwiftUI
struct EditView: View {
#State var entry: Entry?
#ObservedObject var entries: Entries
#State var newDate: Date
#State var newTitle: String
#State var newBody: String
#Environment(\.presentationMode) var presentationMode
init(entries: Entries, entry: Entry?) {
UIScrollView.appearance().keyboardDismissMode = .onDrag
_entry = .init(initialValue: entry)
_entries = .init(initialValue: entries)
_newDate = .init(initialValue: entry?.date ?? Date())
_newTitle = .init(initialValue: entry?.title ?? "")
_newBody = .init(initialValue: entry?.body ?? "")
}
var body: some View {
GeometryReader { geometry in
Form {
Section {
DatePicker("Date", selection: self.$newDate, in: ...Date(), displayedComponents: .date)
.labelsHidden()
}
Section {
TextField("Title (optional)", text: self.$newTitle)
TextView(placeholder: "Entry", text: self.$newBody)
.frame(width: geometry.size.width, height: 250, alignment: .topLeading)
}
}
}
.navigationBarItems(trailing:
Button("Done") {
if let entry = self.entry {
if let index = self.entries.list.firstIndex(of: entry) {
self.entries.list[index] = Entry(date: self.newDate, title: self.newTitle, body: self.newBody)
}
} else {
self.entries.list.append(Entry(date: self.newDate, title: self.newTitle, body: self.newBody))
}
self.presentationMode.wrappedValue.dismiss()
})
}
}
import Foundation
class Entries: ObservableObject {
#Published var list = [Entry]()
}
class Entry: ObservableObject, Identifiable, Equatable {
static func == (lhs: Entry, rhs: Entry) -> Bool {
return lhs.id == rhs.id
}
let id = UUID()
#Published var date: Date
var dateString: String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter.string(from: self.date)
}
#Published var title: String
#Published var body: String
init(date: Date, title: String, body: String) {
self.date = date
self.title = title
self.body = body
}
static let example = Entry(date: Date(), title: "I wrote some swift today", body: "Today I wrote some swift for an app I'm developing. It was very fun.")
When I remove the self.presentationMode.wrappedValue.dismiss() line, the problem goes away. Though, I need that line to dismiss the view. Why would this be happening, and how can I fix it? Please forgive me if my code is a complete mess. Thank you!
It looks like it tries to update during dismissing, try to postpone dismiss a bit
DispatchQueue.main.async { // defer to next event
self.presentationMode.wrappedValue.dismiss()
}
How to get startDateOfMonth and endDateOfMonth based on selected date in SwiftUI?
I have found some answers for Swift (DateComponents), but couldn't make it work with SwiftUI.
Why I need this: I am going to use dynamic filters using predicate to filter all the data in the currently selected month (using custom control to switch months). But first I need to get the start and end dates per selected month.
EXAMPLE code:
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var currentDate = Date()
// How to make startDateOfMonth and endDateOfMonth dependent on selected month?
#State private var startDateOfMonth = "1st January"
#State private var endDateOfMonth = "31st January"
var body: some View {
VStack {
DateView(date: $currentDate)
Text("\(currentDate)")
Text(startDateOfMonth)
Text(endDateOfMonth)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DateView.swift
import SwiftUI
struct DateView: View {
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("yyyy MMMM")
return formatter
}()
#Binding var date : Date
var body: some View {
HStack {
Image(systemName: "chevron.left")
.padding()
.onTapGesture {
print("Month -1")
self.changeDateBy(-1)
}
Spacer()
Text("\(date, formatter: Self.dateFormat)")
Spacer()
Image(systemName: "chevron.right")
.padding()
.onTapGesture {
print("Month +1")
self.changeDateBy(1)
}
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.background(Color.yellow)
}
func changeDateBy(_ months: Int) {
if let date = Calendar.current.date(byAdding: .month, value: months, to: date) {
self.date = date
}
}
}
struct DateView_Previews: PreviewProvider {
struct BindingTestHolder: View {
#State var testItem: Date = Date()
var body: some View {
DateView(date: $testItem)
}
}
static var previews: some View {
BindingTestHolder()
}
}
I managed to solve it by the following implementation of ContentView
#State var currentDate = Date()
private var startDateOfMonth: String {
let components = Calendar.current.dateComponents([.year, .month], from: currentDate)
let startOfMonth = Calendar.current.date(from: components)!
return format(date: startOfMonth)
}
private var endDateOfMonth: String {
var components = Calendar.current.dateComponents([.year, .month], from: currentDate)
components.month = (components.month ?? 0) + 1
components.hour = (components.hour ?? 0) - 1
let endOfMonth = Calendar.current.date(from: components)!
return format(date: endOfMonth)
}
var body: some View {
VStack {
DateView(date: $currentDate)
Text("\(currentDate)")
Text(startDateOfMonth)
Text(endDateOfMonth)
}
}
private func format(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
return dateFormatter.string(from: date)
}
Because currentDate is changed by DateView through Binding the body computed property will be invoked thus startDateOfMonth and endDateOfMonth computed properties will return the updated values.