Save string from TextField in UserDefaults with SwiftUI - string

How can the string from a TextField be stored in UserDefaults using SwiftUI?
I have seen this tutorial on how to save the state of a toggle in UserDefaults and that looks promising, but I can't figure out how to use the same idea for storing the content from a TextField: https://www.youtube.com/watch?v=nV-OdfQhStM&list=PLerlU8xuZ7Fk9zRMuh7JCKy1eOtdxSBut&index=3&t=329s
I still want to be able to update the string by typing new text in the TextField, but unless I do that, I want the string to be maintained in the view. Both when changing page and completely exitting the app.

For things like these I suggest you use the .debounce publisher.
import SwiftUI
import Combine
class TestViewModel : ObservableObject {
private static let userDefaultTextKey = "textKey"
#Published var text = UserDefaults.standard.string(forKey: TestViewModel.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: TestViewModel.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
struct ContentView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
TextField("Type something...", text: $viewModel.text)
}
}
The .debounce publisher documentation says:
Use this operator when you want to wait for a pause in the delivery of
events from the upstream publisher. For example, call debounce on the
publisher from a text field to only receive elements when the user
pauses or stops typing. When they start typing again, the debounce
holds event delivery until the next pause.
You don't really want to save the text in the UserDefaults each time the user types something (i.e. for every character he/she types). You just want the text to be ultimately saved to the UserDefaults. The debounce publisher waits for the user to stop typing according to the time you set in the debounce init (in the example above 0.2 secs) and then forwards the event to the sink subscriber that actually saves the text. Always use the debounce publisher when you have a lot of events (for example new text typed in a text field) that may trigger "heavy" operations (whatever you can think of: something related to CoreData, network calls and so forth).

You can create a custom binding as described here and call UserDefaults.standard.set when the text binding is set.
struct ContentView: View {
#State var location: String = ""
var body: some View {
let binding = Binding<String>(get: {
self.location
}, set: {
self.location = $0
// Save to UserDefaults here...
})
return VStack {
Text("Current location: \(location)")
TextField("Search Location", text: binding)
}
}
}
Copied from answer to 'TextField changes in SwiftUI'

Related

Unreliable update with #FetchRequest predicate filtering on UUID typed attributes?

I'm new-ish to Swift, and trying to better understand Core Data with some (I thought!) simple projects.
I'm trying to use an #FetchRequest property wrapper to list child-items which belong to a given parent. This works for most cases, but not when the to-parent relationship is the only thing which would cause a child item to be displayed after a new child item is added.
The problem I'm facing seems to boil down to any filtering done with UUID values using predicates in FetchRequest decorators not being updated the same way other attributes are. The minimal example I've (with an edit) constructed shows just the behaviour when using a UUID for the predicate, and avoids any relationship complications.
My question is, is there a 'nice' way to make this behaviour (UUID in a predicate driving a FetchRequest which updates views properly) work, or something I'm missing, like registering one of the parts as an observed item, or something I'm doing triggering an update at the wrong time?
I have produced an example which illustrates the puzzle I'm having. Note: I'm using iOS 14.4 and Xcode 12.4 I appreciate these are not current, but my machine does not support an OS past Catalina, so I'm stuck here until I can afford a new one.
My Core Data model is as follows:
The Item entity has two attributes (uuid, of type UUID) and an Int16 attribute (number).
The app has two buttons: + to add a new item with a random number value and a fixed UUID of 254DC... and 5 which adds a item, with a number value of 5 (and the same, fixed, UUID).
The predicate[s] I'm using to drive this list filter Item entities which either:
match the hard-coded UUID value*
or have a number attribute of 5
The problem is that when adding (using the + button) a new entity which should show up in this view just because its uuid property matches, it does not appear. It does appear if you quit the app, and return to it (this proves that the predicate does indeed 'match' the UUID values).
After tapping 5 (now showing two rows with number 5, as expected):
After tapping + twice (unchanged view, should have two new rows showing):
If we exit the app and then reopen it, the added Items (300 and 304 in this case) are only now visible:
Different phrasing of the question: Why do these updates work unreliably on UUID, but perfectly with the number (Int16-typed) attribute?
Working as expected:
Deleting Item entities works as expected, regardless of whether they were in the list because of number == 5 or their UUID match
Notes:
I've used viewContext.perform liberally, as one other glitch with an update not displaying was resolved by using that.
I had already (based on this SO question/answer) already tried wrapping each of the elements of the rows as an ObservedObject. This was also suggested in a comment. Have updated the example code to do that, which has no effect on this behaviour. (The elements are not in the items collection produced by the FetchRequest, so do not get wrapped by this code.)
There is lots of try! all over the place. This is just to keep the example code as small as possible.
View code
import SwiftUI
import CoreData
struct ItemList: View {
#Environment(\.managedObjectContext) private var viewContext
static let filterUUID = "254DC821-F654-4D45-BC94-CEEE12A428CB"
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.number, ascending: true)],
predicate: NSPredicate(format: "number == %# OR uuid == %#", NSNumber(5), filterUUID))
var items : FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in ItemRowView(item: item) }
.onDelete(perform: deleteItems)
}
.navigationTitle(Text(items.isEmpty ? "No items" : "Item count: \(items.count)"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button(action: addRandom) {
Label("Add Item", systemImage: "plus")
}
Button(action: addFive) {
Label("Add Five", systemImage: "5.circle.fill")
}
}
}
}
}
private func addItem(number: Int) {
viewContext.perform {
let newItem = Item(context: viewContext)
newItem.uuid = UUID(uuidString: ItemList.filterUUID)
newItem.number = Int16(number)
try! viewContext.save()
}
}
private func addRandom() { addItem(number: Int.random(in: 10...500)) }
private func addFive() { addItem(number: 5) }
private func deleteItems(offsets: IndexSet) {
viewContext.perform {
offsets.map { items[$0] }.forEach(viewContext.delete)
try! viewContext.save()
}
}
}
struct ItemRowView : View {
#ObservedObject var item : Item
var body : some View {
HStack {
Text("\(item.number)")
.font(.largeTitle)
Text(item.uuid?.uuidString.prefix(8) ?? "None")
}
}
}
What have I tried already?
I've spent the best part of two days trawling documentation, StackOverflow, and various blogs, and while there have been several things which seemed on first look to be exactly the answer I was looking for, the simpler I got my example, the more confusing it became.
This started out as a similar issue related to parent-child fetching, but I've narrowed the actual problem I'm facing down to something smaller, which can be demonstrated with the single entity with an UUID, and no relationships.
Supporting code (App and PersistenceController)
import SwiftUI
import CoreData
#main
struct minimalApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ParentListView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "minimal")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
I believe the weird behaviour is due to your predicate, which is being evaluated in two different ways. When you first run the app, or after closing and restarting, the predicate is parsed and passed to SQLite (as a WHERE clause in a SELECT statement). Thereafter, when you add new items, the predicate is evaluated directly in memory (ie in the NSManagedObjectContext) - no need to involve SQLite.
In your predicate, you are comparing a UUID type attribute with a String value - which fails. Even if the string representation of the UUID attribute is the same as the string you compare it with, the context sees them as different and regards the new object as failing the predicate. Hence the view is not updated.
However, SQLite is much more tolerant of type mismatches. (I'm guessing CoreData's implementation of UUID attributes is to store the string representation - but that's just a guess). So when you quit and restart, the fetch is processed by SQLite which regards the new object as meeting the predicate and accordingly includes it in the results.
To get the correct behaviour, I think you need your predicate to compare the UUID attribute with the UUID which has the correct string representation:
NSPredicate(format: "number == %# OR uuid == %#", NSNumber(5), UUID(uuidString: ItemList.filterUUID))

SwiftUI ColorPicker "opened" and "closed" events?

I need to be able to undo / redo the colors that are picked with the new SwiftUI ColorPicker ( on the iPad : it's presented as a floating window )
The thing that makes it very difficult is that there is apparently no way to know that the user has indeed chosen a color ( and therefore closed the panel )
Instead, the behavior of ColorPicker is that it will keep updating the binded color as the user is manipulating the color controls. This is very helpful to show a live preview, but you don't want to register all these color variations for undo / redo purposes : you only want the color that was finally picked.
Therefore there is no logical distinction between the colors that the user tried, and the one that was selected
And I looked everywhere : there aren't any modifiers / notifications related to that.
I know SwiftUI hasn't been there for long, but this seems like a crucial functionality that's missing?
Has anyone found a workaround?
Undo is always tricky. One size does not fit all. In this case a good solution is to throttle the number of events that come into the UndoManager. You can do so by comparing the current time to the time you last commit.
Create a view model that represents your Source of Truth, and conform to ObservableObject to publish events. It will own the UndoManager. It will have a computed property that gets/sets to the internal data, and check the current time against the time of last commit.
class EditViewModel: ObservableObject {
var commitTime = Date.now
let undoManager = UndoManager()
private var _color: Color = .red {
didSet {
objectWillChange.send()
}
}
var color: Color {
get { _color }
set {
let oldValue = color
let now = Date.now
if now.timeIntervalSince(commitTime) > 1 {
undoManager.registerUndo(withTarget: self) {
$0.color = oldValue
}
}
self.commitTime = now
_color = newValue
}
}
}
Now all that's left to do is create your view and pass in the binding. The implementation details are opaque to the View since it is managed by the ViewModel.
struct EditView: View {
#StateObject var vm = EditViewModel()
var body: some View {
Form {
ColorPicker("Color", selection: $vm.color)
}
}
}
If you need even more control, you can additionally compare the last committed color to the new value and only commit if there is a significant change.
I will also agree that a data-driven approach like SwiftUI's can make undo more tricky. For example, when you need to coalesce multiple operations together into one undo group. By its very nature an undo group is procedural-- the user did one thing after the other, and finally terminates on some condition. But I'm sure there is some way to encapsulate this step-wise operation in a transaction object of some kind.
Undo is hard!

Why does using FetchedResults work for Text and not TextField? SwiftUI, CoreData/ FetchRequest

I am getting CoreData properties from a FetchRequest and want to use it to pre-populate a text field (a user's Email).
Here is the FetchRequest
#FetchRequest(
entity: Account.entity(),
sortDescriptors:[
NSSortDescriptor(keyPath: \Account.id, ascending: true)
]
)var accounts: FetchedResults<Account>
Note I have all of the proper persistant container and #environment stuff set up to use #FetchRequest like this.
Then in my View stack I have:
var body: some View{
ZStack {
Form {
Section(header: Text("EMAIL")) {
ForEach(accounts, id: \.self) {account in
TextField("Email", text:account.email) // This is where I get an error (see error below)
}
}
}
Error is : Cannot convert value of type 'String' to expected argument type 'Binding<String>'
However is I simply list accounts in a textfield it works. Like so:
var body: some View{
ZStack {
Form {
List(accounts, id: \.self) { account in
Text(account.email ?? "Unknown")
}
}
}
Why does the second code that uses List not give me the same error?
I thought it had something to do with the ?? operator but after research I realized that it perfectly fine to do given that email in my coredata object is String?.
Now my thought is that I am getting this error here because TextField needs a Binding wrapper? If that is true I'm not sure how to get this to work. All I want to do is have this TextField pre-populated with the single email record the FetchRequest retrieves from my Account Core Data object.
Thanks.
Edit: I want to add that I have found this post https://www.hackingwithswift.com/quick-start/swiftui/how-to-fix-cannot-convert-value-of-type-string-to-expected-argument-type-binding-string
and now I think what I need to do is store this account.email result into a State variable. My question still remains however, I'm not sure how to do this as I am looking for clean way to do it right in the view stack here. Thanks again.
TextField needs a binding to a string variable. Account is and ObservableObject, like all NSManagedObjects, so if you refactor your TextField into a separate View you could do this:
Form {
Section(header: Text("EMAIL")) {
ForEach(accounts, id: \.self) { account in
Row(account: account)
}
}
}
...
struct Row: View {
#ObservedObject var account: Account
var body: some View {
TextField("Email", text: $account.email)
}
}
Note the $account.email — the $ turns this into a binding.
Unfortunately, this new code also fails, because email is a String? (i.e., it can be nil) but TextField doesn’t allow nil. Thankfully that’s not too hard to fix, because we can define a custom Binding like so:
struct Row: View {
#ObservedObject var account: Account
var email: Binding<String> {
Binding<String>(get: {
if let email = account.email {
return email
} else {
return "Unknown"
}
},
set: { account.email = $0 })
}
var body: some View {
TextField("Email", text: email)
}
}
Notice we don’t have a $ this time, because email is itself a Binding.
This answer details getting a CoreData request into a #State variable:
Update State-variable Whenever CoreData is Updated in SwiftUI
What you'll end up with is something like this:
#State var text = ""
In your view (maybe attached to your ZStack) an onReceive property that tells you when you have new CoreData to look at:
ZStack {
...
}.onReceive(accounts.publisher, perform: { _ in
//you can reference self.accounts at this point
//grab the index you want and set self.text equal to it
})
However, you'll have to do some clever stuff to make sure setting it the first time and then probably not modifying it again once the user starts typing.
This could also get complicated by the fact that you're in a list and have multiple accounts -- at this point, I may split every list item out into its own view with a separate #State variable.
Keep in mind, you can also write custom bindings like this:
Binding<String>(get: {
//return a value from your CoreData model
}, set: { newValue in })
But unless you get more clever with how you're returning in the get section, the user won't be able to edit the test. You could shadow it with another #State variable behind the scenes, too.
Finally, here's a thread on the Apple Developer forums that gets even more in-depth about possible ways to address this: https://developer.apple.com/forums/thread/128195

Swift 3 Fetch Request Error (warning: could not load any Objective-C class information)

I recently received this error when fetching data from Core Data:
warning: could not load any Objective-C class information. This will significantly reduce the quality of type information available.
(lldb)
Here is my code:
// MARK: - Initialize Fetch Request
var fetchedResultsController = NSFetchedResultsController<Profile>()
func setFetchRequest() -> NSFetchRequest<Profile> {
let request = Profile.fetchRequest()
let sortDescriptor = SortDescriptor(key: "title", ascending: false)
do {
try moc?.fetch(request)
} catch {
print("Error With Request: \(error)")
}
request.sortDescriptors = [sortDescriptor]
return setFetchRequest()
}
// MARK: - Retrieve Fetch Request
func getFetchRequest() -> NSFetchedResultsController<Profile> {
fetchedResultsController = NSFetchedResultsController(fetchRequest: setFetchRequest(), managedObjectContext: moc!, sectionNameKeyPath: nil, cacheName: nil)
return fetchedResultsController
}
I crashed with this error where I have "try moc?.fetch(request)":
Thread 1 EXC_BAD_ACCESS (code=2, address=0x16fc07feo)
Are these errors connected or is this a bug in Swift 3 / Xcode 8?
You shouldn't take results from the ManagedObjectContext. If you want to use a NSFetchedResultsController class in your app? You'll need to access their methods. And all of the required or optional methods are comes from the NSFetchedResultsControllerDelegate protocol.
Try this
class YourTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
var fetchedResultsController:NSFetchedResultsController<Profile>!
}
And then create a custom helper function like this one:
`func frc() {
let request:NSFetchRequest<Profile> = Profile.fetchRequest()
let sorter = SortDescriptor(key: "title", ascending: true)
request.sortDescriptors = [sorter]
self.fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
// make sure the delegate is set to self
self.fetchedResultsController.delegate = self
do {
try self.fetchedResultsController.performFetch()
} catch {}
}
`
From this point you'll need a trigger to perform operations. So let's the system itself should be doing this when you call the viewDidLoad method or you can create a button instead. For example click the button to begin operations.
override func viewDidLoad() {
super.viewDidLoad()
self.frc()
self.tableView.reloadData()
}
It should be works.
Good luck!
Automatic Subclass Generation
Xcode 8 and Swift 3 comes with a new generation of subclassing called as Automatic Subclass Generation! How to create it? Well! So let's create a new Xcode 8 project, choose a Single View Application and then another window will appears called Choose options for your new project:. Give the name for your new project, make sure language is a Swift and Use Core Data check box is checked and then hit Create.
Go to the YourProjectName.xcdatamodeld file and click it. And then add an entity! So let's say your entity name is a Profile and create their Attributes respectively. It's important to know, because this is an Automatic Subclass Generation. Choose your entity and go to the Data Model Inspector ! Choose a Class Definition for the Codegen You can find a Codegen from here.
After selected the Class Definition, you can see Name text field automatically filled by your entity name like so. Again go to the your entity and click it. Click Command + S for save changes firstly and then click Command + B for rebuild, that's it. Automatic Subclass Generation is successfully created.
Remember
If you want to change your model? For example: If you want to add a new Attribute to your model? It's very easy, select a xcdatamodeld file and click your entity. Click the plus sign under Attributes and add your new Attribute. After your changes is completed? Don't forget to save changes. Again click Command + S and then click Command + B
Create A Managed Object
In the Swift 3 you can create a ManagedObject by using subclass initializer. It's very easy to implementing than ever before
let managedObject = Profile(context: self.managedObjectContext!)
You can see it's very easy! How to save values to the managedObject ? So let's say you have a title attribute of your model. Also title is a String.
managedObject.setValue("Well crafted API? Right?", forKey: "title")
or
managedObject.title = "Well crafted API? Right?"
Save values:
do {
try self.managedObjectContext.save()
print(managedObject)
} catch {}
It's works well on the Swift 3.0 beta and Xcode 8.0 beta.
Update
So, this is what I got working for Xcode 8 beta and Swift 3 beta Core Data
var fetchedResultsControler = NSFetchedResultsController<Profile>()
func frc() {
let request: NSFetchRequest<Profile> = Profile.fetchRequest()
let sortDescriptor = SortDescriptor(key: "title", ascending: true)
request.sortDescriptors = [sortDescriptor]
self.fetchedResultsControler = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.moc!, sectionNameKeyPath: nil, cacheName: nil)
self.fetchedResultsControler.delegate = self
do {
try self.fetchedResultsControler.performFetch()
} catch {
print("Error Fetching Data: \(error)")
}
}
and in viewDidLoad I have self.frc() at the top of the viewDidLoad.
So, in my Profile+CoreDataProperties.swift I copied a method Apple uses in their Master-Detail example when you create a new project:
extension Profile {
#nonobjc class func fetchRequest() -> NSFetchRequest<Profile> {
return NSFetchRequest<Profile>(entityName: "Profile");
}
#NSManaged var title: String?
#NSManaged var titleImage: Data
}
so that my fetch request is "native to my function." Pretty sure that's not the correct way to say that but it's helping me understand what is going on. The fetch request in Apple's example is green instead of blue. And it took me forever to notice that. I clicked on "Event" in Apple's example, and was conveniently taken to the created subclass, which was demonstrated in the Xcode 8 video at WWDC.
The files for e.g. Event.swift and Event+CoreDataProperties.swift are not exposed like they are in Xcode 7.x.x and earlier. You have to click on the entity in the code and you'll be taken to them. Maybe that was my problem? Anyway, I'm fetching data and images like a champ. Thanks a lot for your help #Mannopson!

QML Strings in BlackBerry Cascades

I am trying to build a custom button in newest BlackBerry 10 platform.
The button should change background image when it is clicked and then change it back when it is clicked the second time.
The button logic is fairly simple: once clicked, I check for the type of image currently in the button and change the image source.
I started with a basic QML custom control which looks like this (stripped of labels and other unimportant things):
import bb.cascades 1.0
Container
{
id: root
layout: DockLayout
{
}
function clickMe()
{
var source = myImage.defaultImageSource.toString();
console.log(source);
if (source.endsWith("image.png"))
{
myImage.defaultImageSource = "asset:///images/image_pushed.png";
}
else
{
myImage.defaultImageSource = "asset:///images/image.png";
}
}
ImageButton
{
id: myImage
defaultImageSource: "asset:///images/image.png"
}
onCreationCompleted:
{
myImage.clicked.connect(root.clickMe);
}
}
ImageButton click event is connected to JavaScript function clickMe. The function fires and the URL is logged to console correctly.
The problem is the IF clause, because the image_pushed.png is never set. Why is this the problem and how can I implement this button?
I am looking around for a only QML solution for this problem and I found this information:
the defaultImageSource property is of type QUrl, which does contain
toString() method.
toString() method returns QString, which indeed has function endsWith.
my QML reference: http://qt-project.org/doc/qt-4.8/qstring.html#endsWith
Thanks.
Within QML QString instances appear to be a normal JavaScript strings. This mapping is done automatically. And Javascript strings don't have a endsWith method. You can use the search method with an regular expression to achieve the same.
if (source.search(/image\.png$/ !== -1) { /* ... */ }
I think you can create more simple way by using property
for example:
Control{
id : myControl
property bool state
ImageButton{
defaultImageSource : state ? "firstImageAsset.png" : "secondImageAsset.png"
onClick :{
myControl.state = !myControl.state
}
}
}

Resources