I'm using the DecimalField struct to place text fields in my app. However, if I use it alongside an environment object, the app freezes with a memory leak.
This is my model:
class PaymentPlan: ObservableObject {
#Published var amountBorrowed: Decimal?
}
This is my content view:
var currencyFormatter: NumberFormatter {
let nf = NumberFormatter()
nf.numberStyle = .currency
nf.isLenient = true
return nf
}
struct ContentView: View {
#EnvironmentObject var paymentPlan: PaymentPlan
static var currencyFormatter: NumberFormatter {
let nf = NumberFormatter()
nf.numberStyle = .currency
nf.isLenient = true
return nf
}
var body: some View {
DecimalField("Placeholder", value: $paymentPlan.amountBorrowed, formatter: Self.currencyFormatter)
}
}
This is the custom text field I am using (source):
import SwiftUI
import Combine
struct DecimalField : View {
let label: LocalizedStringKey
#Binding var value: Decimal?
let formatter: NumberFormatter
let onEditingChanged: (Bool) -> Void
let onCommit: () -> Void
// The text shown by the wrapped TextField. This is also the "source of
// truth" for the `value`.
#State private var textValue: String = ""
// When the view loads, `textValue` is not synced with `value`.
// This flag ensures we don't try to get a `value` out of `textValue`
// before the view is fully initialized.
#State private var hasInitialTextValue = false
init(
_ label: LocalizedStringKey,
value: Binding<Decimal?>,
formatter: NumberFormatter,
onEditingChanged: #escaping (Bool) -> Void = { _ in },
onCommit: #escaping () -> Void = {}
) {
self.label = label
_value = value
self.formatter = formatter
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
}
var body: some View {
TextField(label, text: $textValue, onEditingChanged: { isInFocus in
// When the field is in focus we replace the field's contents
// with a plain unformatted number. When not in focus, the field
// is treated as a label and shows the formatted value.
if isInFocus {
self.textValue = self.value?.description ?? ""
} else {
let f = self.formatter
let newValue = f.number(from: self.textValue)?.decimalValue
self.textValue = f.string(for: newValue) ?? ""
}
self.onEditingChanged(isInFocus)
}, onCommit: {
self.onCommit()
})
.onReceive(Just(textValue)) {
guard self.hasInitialTextValue else {
// We don't have a usable `textValue` yet -- bail out.
return
}
// This is the only place we update `value`.
self.value = self.formatter.number(from: $0)?.decimalValue
}
.onAppear(){ // Otherwise textfield is empty when view appears
self.hasInitialTextValue = true
// Any `textValue` from this point on is considered valid and
// should be synced with `value`.
if let value = self.value {
// Synchronize `textValue` with `value`; can't be done earlier
self.textValue = self.formatter.string(from: NSDecimalNumber(decimal: value)) ?? ""
}
}
.keyboardType(.decimalPad)
}
}
Any suggestions on what may not be working well? The text field works perfectly with #State.
Here is fixed part - to avoid cycling it needs to update only with really new value
Tested with Xcode 12 / iOS 14
.onReceive(Just(textValue)) {
guard self.hasInitialTextValue else {
// We don't have a usable `textValue` yet -- bail out.
return
}
// This is the only place we update `value`.
let newValue = self.formatter.number(from: $0)?.decimalValue
if newValue != self.value {
self.value = newValue
}
}
Related
I want to record long term, how many times a specific ItemView has been displayed in my TabView below. Each time a user swipes on the tab, I want to update var timesViewed by 1. However, timesViewed doesn't seem to update and I am really stuck as to why now.
I removed some view modifiers to simplify the code below.
struct Item: Identifiable, Hashable, Codable {
var id = UUID()
var title: String
var detail: String
var repeatOn: String
var timesViewed = 0
mutating func viewedThisItem() {
timesViewed += 1
}
}
struct ItemSessionView: View {
var itemViewModel: ItemListVM
#State var count = 0
#State var currentIndex = 0
var body: some View {
let today = getTodaysDate().uppercased()
var tempList = itemViewModel.list.filter({ return $0.repeatOn == today})
ZStack {
GeometryReader { proxy in
TabView(selection: $currentIndex) {
ForEach(tempList) { item in
Group {
if today == item.repeatOn {
ItemDetailView(item: item)
}
}
}
}
.onChange(of: currentIndex) { value in
tempList[currentIndex].viewedThisItem()
}
}
}
}
func getTodaysDate() -> String {
let today = Date.now
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
formatter.dateFormat = "E"
let todaysDate = formatter.string(from: today)
return todaysDate
}
}
Structs are value type, you modify the (copied) item in the filtered array but not the original item in the itemViewModel object.
A possible solution is to get the item in the itemViewModel object by id and modify that directly.
.onChange(of: currentIndex) { value in
let id = tempList[value].id
let index = itemViewModel.list.firstIndex{$0.id == id}!
itemViewModel.list[index].viewedThisItem()
}
Force unwrapping is safe because the item does exist.
I tried to build a NumberField from a TextField where the value is validated and pushed along the Binding only when the .onSubmit modifier is called. The logic seems to work fine but you will see that the NumberField is not updated properly when the submitted value is outside the specified range.
Here's my code for the View:
struct NumberField: View {
init(
value: Binding<Double>,
range: ClosedRange<Double>
) {
self._value = value
self.range = range
self._valueStore = State(initialValue: value.wrappedValue)
}
#Binding var value: Double
let range: ClosedRange<Double>
#State private var valueStore: Double
var body: some View {
TextField(
"",
value: .init(
get: {
valueStore
},
set: { newValue in
valueStore = validate(
newValue,
range: range
)
}),
formatter: NumberFormatter()
)
.onSubmit {
value = valueStore
}
.onChange(of: value) { newValue in
guard newValue != valueStore else {
return
}
valueStore = newValue
}
}
func validate(_ newValue: Double, range: ClosedRange<Double>) -> Double {
let validatedValue = clamp(newValue, to: range)
print("validate - newValue: \(newValue) - validated: \(validatedValue)")
return validatedValue
}
func clamp(_ value: Double, to range: ClosedRange<Double>) -> Double {
min(max(range.lowerBound, value), range.upperBound)
}
}
You can use it like so in a playground (or a macOS app):
struct MainView: View {
#State var value = 2.0
var body: some View {
VStack {
Text("value: \(value)")
NumberField(value: $value, range: 2.0 ... 10)
NumberField(value: $value, range: 2.0 ... 10)
}
.frame(width: 200)
.padding()
}
}
Using Xcode 13.4.1 on macOS 12.5. I revised the working code to conform to MVVM. This was successful for the first Entity (all properties Optional) for all CRUD operations.
Using this code as a base, I tackled the second Entity (one Bool property NOT Optional), but it throws the compiler error inside the ForEach loop, against 'contact'. This code was error-free before the MVVM conversion. I've been at this for 4 days and am reaching out, but clearly my limited knowledge is inadequate.
ContactListView code below, supported by the ContactViewModel, which in turn relies on the CoreDataManager code.
import SwiftUI
import CoreData
//class FirstNameSort: ObservableObject {
// #Published var firstNameSort: Bool = false
//}
struct ContactsListView: View {
// MARK: - PROPERTIES
#Environment(\.managedObjectContext) var viewContext
#ObservedObject var contactVM = ContactViewModel()
#State private var totalContacts: Int = 0
#State private var search: String = ""
#State private var searchByChampions = false
#State var searchByFirstNames = false
#State private var totalChampions = 0
// MARK: - BODY
var body: some View {
NavigationView {
VStack {
// HStack {
// Toggle("Display Champions only", isOn: $searchByChampions)
// .toggleStyle(.switch)
// .foregroundColor(.blue)
// .padding()
// Toggle("Sort by First Names", isOn: $contactVM.sortFirstName)
// .toggleStyle(.switch)
// .foregroundColor(.blue)
// .padding()
//}
List {
HStack {
Text(searchByChampions ? "Total Champions" : "Total Contacts")
.foregroundColor(.gray)
Spacer()
Text("\(searchByChampions ? totalChampions : totalContacts)")
.bold()
}.foregroundColor(.green)
.padding()
ForEach($contactVM.listofContacts) { contact in
NavigationLink(destination:
ModifyContactView(contact: ***contact***)
.id(UUID()), label: {
ContactRowView(contact: ***contact***)
.id(UUID())
})
}
.onDelete(perform: contactVM.deleteContact)
}.navigationTitle("Contacts")
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: AddContactView(), label: {
Image(systemName: "plus.circle")
})
}
}
.onAppear {
countContacts()
countChampions()
}
.searchable(text: $search, prompt: Text("Contact Last Name?"))
// .onChange(of: search) { value in
// if !value.isEmpty {
// listofContacts.nsPredicate = NSPredicate(format: "contactLastName CONTAINS[dc] %#", value)
// } else {
// listofContacts.nsPredicate = nil
// }
// }
}
}.navigationViewStyle(.stack)
}
func countContacts() {
totalContacts = $contactVM.listofContacts.count
}
// func countChampions() {
// totalChampions = $contactVM.listOfChampions.count
// }
}
import CoreData
import SwiftUI
class ContactViewModel: ObservableObject {
#Environment(\.dismiss) var dismiss
#ObservedObject var dataVM = CoreDataManager()
#ObservedObject var qualifierVM = QualifierViewModel()
#Published var inputFirstName: String = ""
#Published var inputLastName: String = ""
#Published var inputCellNumber: String = ""
#Published var inputEmail: String = ""
#Published var inputChampion: Bool = false
#Published var inputComments: String = ""
#Published var inputCreated: Date = Date()
#Published var inputUpdated: Date = Date()
#Published var listOfFirstNames = []
#Published var listofContacts: [ContactEntity] = []
func fetchContacts() {
let request = NSFetchRequest<ContactEntity>(entityName: "ContactEntity")
do {
dataVM.listofContacts = try dataVM.container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addContact(
contactFirstName: String,
contactLastName: String,
contactCellNumber: String,
contactEmail: String,
contactChampion: Bool,
contactComments: String,
contactCreated: Date,
contactUpdated: Date) {
let newContact = ContactEntity(context: dataVM.container.viewContext)
newContact.contactFirstName = contactFirstName
newContact.contactLastName = contactLastName
newContact.contactCellNumber = contactCellNumber
newContact.contactEmail = contactEmail
newContact.contactChampion = contactChampion
newContact.contactComments = contactComments
newContact.contactUpdated = Date()
newContact.contactCreated = Date()
let uniqueClient = Set(dataVM.selectedClient)
for client in uniqueClient {
newContact.addToClients(client)
print("Client: \(client.clientName ?? "No client")")
}
saveContact()
dismiss()
}
func deleteContact(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = dataVM.listofContacts[index]
dataVM.container.viewContext.delete(entity)
saveContact()
}
func saveContact() {
do {
try dataVM.container.viewContext.save()
fetchContacts()
} catch let error {
print("Error saving. \(error)")
}
}
func sortLastName() -> [ Array<Any>] {
let listOfLastNames = dataVM.listofContacts.sorted {
$0.contactLastName ?? "" < $1.contactLastName ?? ""
}
return [listOfLastNames]
}
func sortFirstName() -> [ Array<Any>] {
let listOfFirstNames = dataVM.listofContacts.sorted {
$0.contactFirstName ?? "" < $1.contactFirstName ?? ""
}
return [listOfFirstNames]
}
}
import Foundation
import CoreData
class CoreDataManager: ObservableObject {
let container: NSPersistentContainer
#Published var listOfQualifiers: [QQEntity] = []
#Published var listofContacts: [ContactEntity] = []
#Published var listOfClients: [ClientEntity] = []
#Published var listOfOpportunities: [OpportunityEntity] = []
//#Published var selectedClient: [ClientEntity] = []
init() {
container = NSPersistentContainer(name: "B2BContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error loading Core Data. \(error)")
} else {
print("Successfully loaded Core Data...")
}
}
}
}
I tested my app with Instruments and found no leaks but memory increases every time any value is updated and My app is updated every 0.5 second in multiple places. After running for about 20 mins, My app crashed and i got "Terminated due to memory issue" message.
I tried to find out which place cause this issue and ItemView seems to be causing the problem. I created a code snippet and the test result is below.
Please explain what's wrong with my code.
Thanks for any help!
import SwiftUI
import Combine
struct ContentView: View {
#State private var laserPower: Float = 0
var body: some View {
VStack {
ItemView(
title: "Laser Power",
value: $laserPower,
unit: "W",
callback: { (newValue) in
print("setPower")
//Api.setPower(watts: newValue)
}
)
}
.onAppear {
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { _ in
//Memory increases every time the laserPower's value is updated.
laserPower = Float(Int.random(in: 1..<160) * 10)
}
}
}
}
struct ItemView: View {
let title: String
#Binding var value: Float
let unit: String
let callback: (_ newValue: Float) -> Void
#State private var stringValue = ""
#State private var isEditingValue = false
#State var editingValue: Float = 0
func getStringValue(from value: Float) -> String {
return String(format: "%.1f", value)
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
Text(title)
Spacer()
}
HStack {
TextField(
"",
text: $stringValue,
onEditingChanged: { editingChanged in
DispatchQueue.main.async {
if !editingChanged {
if stringValue.contain(pattern: "^-?\\d+\\.\\d$") {
editingValue = Float(stringValue)!
} else if stringValue.contain(pattern: "^-?\\.\\d$") {
stringValue = "0.\(stringValue.split(separator: ".", omittingEmptySubsequences: false)[1])"
editingValue = Float(stringValue)!
} else if stringValue.contain(pattern: "^-?\\d+\\.?$") {
stringValue = "\(stringValue.split(separator: ".", omittingEmptySubsequences: false)[0]).0"
editingValue = Float(stringValue)!
} else {
stringValue = getStringValue(from: value)
}
callback(Float(getStringValue(from: editingValue))!)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isEditingValue = false
}
} else {
isEditingValue = true
}
}
},
onCommit: {
DispatchQueue.main.async {
callback(Float(getStringValue(from: editingValue))!)
}
}
)
.font(.title)
.multilineTextAlignment(.center)
.padding(4)
.overlay(RoundedRectangle(cornerRadius: 5).stroke())
Text(unit)
.padding(5)
}
}
.padding(10)
.onReceive(Just(stringValue)) { newValue in
if newValue.contain(pattern: "^-?\\d+\\.\\d?$") {
return
}
var filtered = newValue.filter { "0123456789.-".contains($0) }
let split = filtered.split(separator: ".", omittingEmptySubsequences: false)
if (split.count > 1 && String(split[1]).count > 1) || split.count > 2 {
let dec = split[1]
filtered = "\(split[0]).\(dec.isEmpty ? "" : String(dec[dec.startIndex]))"
}
self.stringValue = filtered
}
.onChange(of: value) { _ in
if !isEditingValue {
stringValue = getStringValue(from: self.value)
editingValue = value
}
}
.onAppear {
stringValue = getStringValue(from: value)
editingValue = value
}
}
}
extension String {
func contain(pattern: String) -> Bool {
guard let regex = try? NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options()) else {
return false
}
return regex.firstMatch(in: self, options: NSRegularExpression.MatchingOptions(), range: NSMakeRange(0, self.count)) != nil
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Memory report:
Instruments report:
I have two columns with nested data(Parent/child). Each item in first column is parent. When selecting anyone of them then it shows its child in second column as list.
When selecting any item from second column then it must show "clipAttr" attribute in third column as text editor where we can edit it.
Now I need help how to do that when edit the 'ClipAttr' then it automatically update in SampleDataModel collection. Below is the complete code.
struct SampleClip: Identifiable, Hashable {
var uid = UUID()
var id :String
var itemType:String?
var clipTitle: String?
var creationDate: Date?
var clipAttr:NSAttributedString?
}
struct SampleClipset: Identifiable, Hashable {
var id = UUID()
var clipsetName :String
var isEditAble:Bool
init( clipsetName:String, isEditAble:Bool){
self.clipsetName = clipsetName
self.isEditAble = isEditAble
}
}
struct SampleClipItem: Identifiable, Hashable {
var id = UUID()
var clipsetObject: SampleClipset
var clipObjects: [SampleClip]
}
class SampleDataModel: ObservableObject {
#Published var dict:[SampleClipItem] = []
#Published var selectedItem: SampleClipItem? {
didSet {
if self.selectedItem != nil {
if( self.selectedItem!.clipObjects.count > 0){
self.selectedItemClip = self.selectedItem!.clipObjects[0]
}
}
}
}
#Published var selectedItemClip: SampleClip? {
didSet {
if self.selectedItemClip != nil {
}
}
}
}
struct SampleApp: View {
#ObservedObject var vm = SampleDataModel()
#State var clipText = NSAttributedString(string: "Enter your text")
var body: some View {
VStack {
//Button
HStack{
//Clipset button
VStack{
Text("Add Parent data")
.padding(10)
Button("Add") {
let clipset1 = SampleClipset(clipsetName: "Example clipset\(self.vm.dict.count)", isEditAble: false)
var clip1 = SampleClip(id: "0", itemType: "", clipTitle: "Clip 1")
clip1.clipAttr = NSAttributedString(string: clip1.clipTitle!)
clip1.creationDate = Date()
var clip2 = SampleClip(id: "1", itemType: "", clipTitle: "Clip 2")
clip2.clipAttr = NSAttributedString(string: clip2.clipTitle!)
clip2.creationDate = Date()
let item = SampleClipItem(clipsetObject: clipset1, clipObjects: [clip1, clip2] )
self.vm.dict.append(item)
}
Button("Update") {
let index = self.vm.dict.count - 1
self.vm.dict[index].clipsetObject.clipsetName = "Modifying"
}
}
Divider()
//Clip button
VStack{
Text("Add Child data")
.padding(10)
Button("Add") {
let object = self.vm.dict.firstIndex(of: self.vm.selectedItem!)
if( object != nil){
let index = self.vm.selectedItem?.clipObjects.count
var clip1 = SampleClip(id: "\(index)", itemType: "", clipTitle: "Clip \(index)")
clip1.clipAttr = NSAttributedString(string: clip1.clipTitle!)
clip1.creationDate = Date()
self.vm.dict[object!].clipObjects.append(clip1)
self.vm.selectedItem = self.vm.dict[object!]
}
}
Button("Update") {
let index = (self.vm.selectedItem?.clipObjects.count)! - 1
self.vm.selectedItem?.clipObjects[index].clipAttr = NSAttributedString(string:"Modifying")
}
}
}.frame(height: 100)
//End button frame
//Start Column frame
Divider()
NavigationView{
HStack{
//Clipset list
List(selection: self.$vm.selectedItem){
ForEach(Array(self.vm.dict), id: \.self) { key in
Text("\(key.clipsetObject.clipsetName)...")
}
}
.frame(width:200)
.listStyle(SidebarListStyle())
Divider()
VStack{
//Clip list
if(self.vm.selectedItem?.clipObjects.count ?? 0 > 0){
List(selection: self.$vm.selectedItemClip){
ForEach(self.vm.selectedItem!.clipObjects, id: \.self) { key in
Text("\(key.clipTitle!)...")
}
}
.frame(minWidth:200)
}
}
//TextEditor
Divider()
SampleTextEditor(text: self.$clipText)
.frame(minWidth: 300, minHeight: 300)
}
}
}
}
}
struct SampleApp_Previews: PreviewProvider {
static var previews: some View {
SampleApp()
}
}
//New TextView
struct SampleTextEditor: View, NSViewRepresentable {
typealias Coordinator = SampleEditorCoordinator
typealias NSViewType = NSScrollView
let text : Binding<NSAttributedString>
func makeNSView(context: NSViewRepresentableContext<SampleTextEditor>) -> SampleTextEditor.NSViewType {
return context.coordinator.scrollView
}
func updateNSView(_ nsView: NSScrollView, context: NSViewRepresentableContext<SampleTextEditor>) {
if ( context.coordinator.textView.textStorage != text.wrappedValue){
context.coordinator.textView.textStorage?.setAttributedString(text.wrappedValue)
}
}
func makeCoordinator() -> SampleEditorCoordinator {
let coordinator = SampleEditorCoordinator(binding: text)
return coordinator
}
}
class SampleEditorCoordinator : NSObject, NSTextViewDelegate {
let textView: NSTextView;
let scrollView : NSScrollView
let text : Binding<NSAttributedString>
init(binding: Binding<NSAttributedString>) {
text = binding
textView = NSTextView(frame: .zero)
textView.autoresizingMask = [.height, .width]
textView.textStorage?.setAttributedString(text.wrappedValue)
textView.textColor = NSColor.textColor
//Editor min code
textView.isContinuousSpellCheckingEnabled = true
textView.usesFontPanel = true
textView.usesRuler = true
textView.isRichText = true
textView.importsGraphics = true
textView.usesInspectorBar = true
textView.drawsBackground = true
textView.allowsUndo = true
textView.isRulerVisible = true
textView.isEditable = true
textView.isSelectable = true
textView.backgroundColor = NSColor.white
//
scrollView = NSScrollView(frame: .zero)
scrollView.hasVerticalScroller = true
scrollView.autohidesScrollers = false
scrollView.autoresizingMask = [.height, .width]
scrollView.documentView = textView
super.init()
textView.delegate = self
}
func textDidChange(_ notification: Notification) {
switch notification.name {
case NSText.didChangeNotification :
text.wrappedValue = (notification.object as? NSTextView)?.textStorage ?? NSAttributedString(string: "")
default:
print("Coordinator received unwanted notification")
//os_log(.error, log: uiLog, "Coordinator received unwanted notification")
}
}
}
First use custom Binding.
SampleTextEditor(text: Binding(get: {
return self.vm.selectedItemClip?.clipAttr
}, set: {
self.vm.selectedItemClip?.clipAttr = $0
}))
Second, update your view on child update button.
Button("Update") {
guard let mainIndex = self.vm.dict.firstIndex(where: { (data) -> Bool in
if let selectedId = self.vm.selectedItem?.id {
return data.id == selectedId
}
return false
}),
let subIndex = self.vm.dict[mainIndex].clipObjects.firstIndex(where: { (data) -> Bool in
if let selectedId = self.vm.selectedItemClip?.id {
return data.id == selectedId
}
return false
}),
let obj = self.vm.selectedItemClip
else {
return
}
self.vm.dict[mainIndex].clipObjects[subIndex] = obj
self.vm.selectedItem = self.vm.dict[mainIndex]
}
Inside the SampleEditorCoordinator class and SampleTextEditor struct use optional binding. And change your textDidChange methods.
struct SampleTextEditor: View, NSViewRepresentable {
typealias Coordinator = SampleEditorCoordinator
typealias NSViewType = NSScrollView
let text : Binding<NSAttributedString?>
func makeNSView(context: NSViewRepresentableContext<SampleTextEditor>) -> SampleTextEditor.NSViewType {
return context.coordinator.scrollView
}
func updateNSView(_ nsView: NSScrollView, context: NSViewRepresentableContext<SampleTextEditor>) {
if ( context.coordinator.textView.textStorage != text.wrappedValue){
if let value = text.wrappedValue {
context.coordinator.textView.textStorage?.setAttributedString(value)
}
}
}
// Other code
}
class SampleEditorCoordinator : NSObject, NSTextViewDelegate {
let textView: NSTextView;
let scrollView : NSScrollView
var text : Binding<NSAttributedString?>
init(binding: Binding<NSAttributedString?>) {
text = binding
// Other code
}
func textDidChange(_ notification: Notification) {
switch notification.name {
case NSText.didChangeNotification :
self.text.wrappedValue = NSAttributedString(attributedString: textView.attributedString())
default:
print("Coordinator received unwanted notification")
//os_log(.error, log: uiLog, "Coordinator received unwanted notification")
}
}
}