NSComboBox does not display any text - swift

I have a NSViewController with multiple NSComboBox instances.
I have the following code that adds the items to the NSComboBox and then selects an item:
#IBOutlet weak var monitors: NSComboBox!
fileprivate func fillMonitors()
{
self.monitors.removeAllItems()
var items: [String] = []
for screen in NSScreen.screens
{
items.append(screen.localizedName)
}
self.monitors.addItems(withObjectValues: items)
guard let primaryMonitor = self.configuration?.layout.displayName ?? NSScreen.main?.localizedName else { return }
if let index = items.firstIndex(of: primaryMonitor)
{
self.monitors.selectItem(at: index)
}
}
The items are added and the correct monitor is found which means that the following line selects the proper monitor:
self.monitors.selectItem(at: index)
However, the NSComboBox does not display any text whatsoever. Also, the opening of the dropdown feels very weird, sometimes it's closing again, but then it works properly.
When selecting another item, the event handler is called and the value is processed, so I don't think that this is a bug in my code. Has anyone experienced this before? I have tried to set the text directly using the stringValue but it also does not work. I am really lost here. Really appreciate any hints. I am not using a datasource (property set to false), the delegate is nil.
Using Ventura with Xcode 14.1 RC2.

Related

Can you save the selection of a master-detail bound NSTableView?

I have a manager class for my data which is configured by two properties, one to set to a category and another to select items which correspond with that category. Based on that it will expose the relevant pieces of data. I am using a couple of different forms or making those selections, including a pair of IndexSets.
My problem is that I would also like to be able to save the selected items for each category, so that whenever the category is changed the items previously selected for it are restored. This is easy to achieve when accessed programmatically, but using bindings to allow a view in a macOS app to be able to provide that configuration unfortunately does not work properly
Changing the category causes the object bound to its selection to empty or 'preserve' the selected items before the category is actually updated. So the actual selection gets overwritten with, with noway I can see to tell the difference between this behaviour and a user action.
Here are the test code I have used for experimenting, with viewDidLoad generating some random test data to roughly mimic the structure o the real class. This does not attempt to save or restore the selection, but simply shows the overwriting behaviour.
class Thing: NSObject {
#objc dynamic var name: String
required init(name: String) {
self.name = name
}
}
class Stuff: NSObject {
#objc dynamic var name: String
#objc dynamic var things: [Thing]
required init(name: String, things: [Thing]) {
self.name = name
self.things = things
}
}
class StuffManager: NSObject {
#objc dynamic var stuff = [Stuff]()
#objc dynamic var stuffIndex = IndexSet() {
didSet {
print("STUFF: ", Array(stuffIndex))
}
}
#objc dynamic var things = [Thing]()
#objc dynamic var thingsIndex = IndexSet() {
didSet {
print("THING: ", Array(thingsIndex))
}
}
}
class ViewController: NSViewController {
#objc dynamic var stuffManager = StuffManager()
override func viewDidLoad() {
super.viewDidLoad()
(1...10).forEach { stuffManager.things.append(Thing(name: "Thing \($0)")) }
(1...9).forEach {
let randomThings = Array(stuffManager.things.shuffled()[0...Int.random(in: 0..<10)])
stuffManager.stuff.append(Stuff(name: "Collection \($0)", things: randomThings))
}
stuffManager.stuff.append(Stuff(name: "Collection 10", things: []))
}
}
In Interface Builder I have a view containing an NSPopButton to select the Stuff, a multiple selection NSTableView to select the Things, and a pair of NSArrayControllers for each. The bindings are:
Stuff Array Controller
Content Array:
Binding to: ViewController, Model Key Path: stuffManager.stuff
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.stuffIndex
Things Array Controller
Content Array:
Binding to: Stuff Array Controller, Controller Key: Selection, Model Key Path: things
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.thingIndex
The two interface objects are bound to these controllers in the standard way, the Content to the arrangedObjects and the Selection Indexes to the selectionIndexes of their respective array controller.
What this test code shows is that when the value in the popup button is changed the THING debug line appears before the STUFF debug line, that is it changes the selection of Things before it changes the Stuff. So any code in the property observer on stuffManager.things to save the new selection will save this change before being aware that the Stuff has changed.
Obviously this behaviour is to avoid the selection being made incorrect by the change to the content, or worse selecting out of bounds if the new content is shorter. But is there any way to detect when this is happening, rather than a user changing the selection? Or a way to override it to gain manual control over the process rather than having to accept the default behaviour of 'Preserve Selection' or the selection being cancelled if that option is disabled?
And what makes it more awkward is if this behaviour only occurs when the selection would change. If the selected Things exist for the new Stuff, or if nothing is selected, then nothing happens to trigger the property observer. Again this is understandable, but it prevents being able to cache the change and then only save the previous one if the Stuff has not changed.
I did wonder if using a separate IndexSet for each Stuff would avoid this problem, because then there would be no need for the NSTableView to manage the selection. I do not like the idea of keeping an IndexSet in the model but would accept it if it worked. But it does not. Again understandable, because the table view has no idea the Selection Indexes binding will be changed. Unless I am missing something?
But I tested this by updating the Stuff class to include the following:
#objc dynamic var selected = IndexSet() {
didSet {
print("THING: ", Array(selected))
}
}
Then changing the Selection Indexes binding of the Things Array Controller to:
Binding to: Stuff Array Controller, Controller Key: selection, Model Key Path: selected
Is what I am trying to achieve impossible? I would not have thought it that strange a thing to want to do, to save and restore a selection, but it seems impossible with bindings.
The only solution I can see is to forgo the master-detail style pattern and instead just maintain a separate [Thing] property in my data manager class, bind the Things Array Controller to this (or even just bind the table directly to the property), then whenever the popup button changes update the new property to match the stuff object.
Something like this in the StuffManager, with the table content bound to availableThings:
#objc dynamic var stuffIndex = IndexSet() {
didSet {
print("STUFF: ", Array(stuffIndex))
availableThings = stuff[stuffIndex.first!].things
}
}
#objc dynamic var availableThings = [Thing]()
It appears there is no way to prevent the NSTableView behaviour of automatically resetting its selection when the content changes. Nor any way to detect when this is happening, as it updates this before updating the selection on the NSPopupButton having changed. So here is how I have written the StuffManager class, adding a property for binding to the tableview so I can control the content changing:
class StuffManager: NSObject {
let defaults: UserDefaults = .standard
var canSaveThingsIndex = true
#objc dynamic var stuff = [Stuff]()
#objc dynamic var stuffIndex = IndexSet() {
didSet {
canSaveThingsIndex = false
if stuffIndex.count > 0 {
availableThings = stuff[stuffIndex.first!].things
let thing = stuff[stuffIndex.first!].name
if let items = defaults.object(forKey: thing) as? [Int] {
thingsIndex = IndexSet(items)
} else if availableThings.count > 0 {
thingsIndex = IndexSet(0..<availableThings.count)
} else {
thingsIndex.removeAll()
}
} else {
availableThings.removeAll()
thingsIndex.removeAll()
}
canSaveThingsIndex = true
}
}
#objc dynamic var things = [Thing]()
#objc dynamic var availableThings = [Thing]()
#objc dynamic var thingsIndex = IndexSet() {
didSet {
if canSaveThingsIndex && stuffIndex.count > 0 {
let thing = stuff[stuffIndex.first!].name
defaults.set(Array(thingsIndex), forKey: thing)
}
}
}
}
The Things Array Controller is now bound as:
Content Array:
Binding to: ViewController, Model Key Path: stuffManager.availableThings
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.thingsIndex
Though without being able to use the master-detail benefits of an NSArrayController they are not needed. Both the NSPopupButton and NSTableView can be bound directly to the StuffManager. And this allows the NSPopupButton's Selected Index can be bound to an Int int he Stuff Manager rather than needing to use an IndexSet despite multiple selections being impossible.
The main feature of the workaround is that because I am manually changing the content I can use the canSaveThingsIndex flag before changing the NSTableView content. So whenever its natural behaviour triggers the thingsIndex property observer, this can be ignored to prevent it overwriting the user's selection. It also avoids the unnecessary saving of a selection immediately after being restored.

SwiftUI: Why does #AppStorage work differently to my custom binding?

I have a modal presented Sheet which should display based on a UserDefault bool.
I wrote a UI-Test which dismisses the sheet and I use launch arguments to control the defaults value.
However, when I tried using #AppStorage initially it didn't seem to persist the value, or was secretly not writing it? My test failed as after 'dismissing' the modal pops back up as the value is unchanged.
To work around this I wrote a custom binding. But i'm not sure what the behaviour difference is between the two implementations? The test passes this way.
Does anyone know what i'm not understanding sorry?
Cheers!
Simple Example
1. AppStorage
struct ContentView: View {
#AppStorage("should.show.sheet") private var binding: Bool = true
var body: some View {
Text("Content View")
.sheet(isPresented: $binding) {
Text("Sheet")
}
}
}
2. Custom Binding:
struct ContentView: View {
var body: some View {
let binding = Binding<Bool> {
UserDefaults.standard.bool(forKey: "should.show.sheet")
} set: {
UserDefaults.standard.set($0, forKey: "should.show.sheet")
}
Text("Content View")
.sheet(isPresented: binding) {
Text("Sheet")
}
}
}
Test Case:
final class SheetUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testDismiss() {
// Given 'true' userdefault value to show sheet on launch
let app = XCUIApplication()
app.launchArguments += ["-should.show.sheet", "<true/>"]
app.launch()
// When the user dismisses the modal view
app.swipeDown()
// Wait a second for animation (make sure it doesn't pop back)
sleep(1)
// Then the sheet should not be displayed
XCTAssertFalse(app.staticTexts["Sheet"].exists)
}
}
It does not work even when running app, because of that "." in key name (looks like this is AppStorage limitation, so use simple notation, like isSheet.
IMO the test-case is not correct, because it overrides defaults by arguments domain, but it is read-only, so writing is tried into persistent domain, but there might be same value there, so no change (read didSet) event is generated, so there no update in UI.
To test this it should be paired events inside app, ie. one gives AppStorage value true, other one gives false
*Note: boolean value is presented in arguments as 0/1 (not XML), lie -isSheet 1

swift ui handle property changed in observableObject

so - I have a text field on a screen - the data loads asynchronously - so I've created and ObservableObject with a published field, and successfully bind it to the view:
class Blah : ObservableObject
{
#Published var value : Double? = nil
init()
{
load_variable_async().then {result in self.value = result }
}
}
which works perfectly - the view reflects the value of the variable and everything.
But - I want it to work both ways. Published seems to be a bidirectional wrapper, so I want to add something like this:
init()
{
load_variable_async().then {result in self.value = result }
value.when_changed { new_value in asynchronously_save( variable) }
}
and I can't find any way of doing it. Everything I google for puts a sink or some call to a save in the view - which seems completely wrong to me... if I'm reading it in one place, I want to be writing it in the same place - and if I'm already binding the variable to a textfield for instance, and bindings go both ways, then enough connections have already been made
So what am I doing wrong? How do react to value being set, inside my "model" object without explicitly putting some sort of save or other action into the view?
So I found the solution I wanted - the binding from model -> textfield was always working, but the value from textfield -> model I couldn't get working. The magic seems to be this keyword "willSet"
#Published var value : Double? = nil {
willSet( new_value ) {
print("going to save asynchronously now")
}}
works magically.

Is viewContext different between Swift and SwiftUI

EDIT: I've added a rewording of my question at the bottom.
So I have an app that I've been working on for a long time. I used it to teach myself Xcode / Swift and now that I have a Mac again, I'm trying to learn SwiftUI as well by remaking the app completely as a new project.
I have an Entity in CoreData that only ever contains 1 record, which has all of the attributes of the settings I'm tracking.
Before, I was able to make an extension to my Entity, Ent_Settings that would allow me to always get that specific record back.
class var returnSettings: Ent_Settings {
//this is inside of extension Ent_Settings
let moc = PersistenceController.shared.container.viewContext
var settings: Ent_Settings?
let fetchRequest: NSFetchRequest<Ent_Settings> = Ent_Settings.fetchRequest()
do {
let results = try moc.fetch(fetchRequest)
if results.count == 0 {
Ent_Settings.createSettingsEntity()
do {
let results = try moc.fetch(fetchRequest)
settings = results.first!
} catch {
error.tryError(tryMessage: "Failed performing a fetch to get the Settings object after it was created.", loc: "Ent_Settings Extension")
}
} else {
settings = results.first!
}
} catch {
error.tryError(tryMessage: "Fetching Settings Object", loc: "Ent_Settings Extension")
}
return settings!
}
It took me a while to figure out how to get the moc now that there is no AppDelegate, but you can see I figured out in the code and that seems to work great.
However, when I go to a View that I made in SwiftUI, I'm getting weird results.
In the Simulator, I can run my app and see the changes. Leave the view and come back and see the changes are saved. However, if I rebuild and run the app it loses all data. So it seems it's not remembering things after all. Basically, it's volatile memory instead of persistent memory. But if I put another function in the extension of Ent_Settings to update that attribute and then build and run again, it will persistently save.
I'm wondering if it has something to do with how the context is managed, perhaps it's not seeing the changes since it is a different "listener"?
Here is what I'm doing to make the changes in the View. (I tried removing the extra stuff that just made it harder to see)
import SwiftUI
struct Settings_CheckedView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var picker1 = 0
var body: some View {
Form {
Section(header: Text("..."),
footer: Text("...")) {
VStack {
HStack { ... }
Picker("", selection: $picker1) {
Text("Off").tag(0)
Text("Immediately").tag(1)
Text("Delay").tag(2)
}
.pickerStyle(SegmentedPickerStyle())
.onChange(of: picker1, perform: { value in
settings.autoDeleteSegment = Int16(value)
viewContext.trySave("Updating auto-delete segment value in Settings",
loc: "Ent_Settings Extension")
})
}
}
...
...
.onAppear(perform: {
settings = Ent_Settings.returnSettings
self.picker1 = Int(settings.autoDeleteSegment)
self.picker2 = Int(settings.deleteDelaySegment)
})
}
}
And here is the code that I'm using for the trySave() function I have on the viewContext
extension NSManagedObjectContext {
func trySave(_ tryMessage: String, loc: String) {
let context = self
if context.hasChanges {
do {
try self.save()
} catch {
print("Attempted: \(tryMessage) -in \(loc)")
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
This code doesn't seem to actually save anything to the Database.
I also tried adding this code into Settings_CheckedView to see if that would make a difference, but I got even weird results.
#FetchRequest(sortDescriptors:[])
private var settingsResult: FetchedResults<Ent_Settings>
But that returns zero results even though I know the Ent_Settings has 1 record. So that makes me wonder if I am actually creating two databases with the way I'm doing it.
I'm really new to SwiftUI and CoreData seems to be one of those things that is hard to get information about. Hoping someone can point out where I'm going wrong. I'd love to be able to make CoreData changes in both Swift and SwiftUI.
Thank you for any help you can give and let me know if you need anything else added in.
EDITED: To try to reword question
So ultimately, I'm wanting to do some stuff with CoreData inside of the extension for that entity. My database has a lot of aspects to it, but I also made an entity in there to store settings information and since that one isn't related to anything else, I figured that would be a good starting point in learning SwiftUI. I have everything working in UIKit, but I'm specifically trying to learn SwiftUI as well.
From how I understand SwiftUI, it is meant to replace the storyboard and make the lifecycle stuff much easier. So classes I have that deal with CoreData should still do that external to SwiftUI.
As you can see above in my Settings_CheckedView view above, I'm referencing that single settings record from an extension in Ent_Settings. Basically, everything inside of that extension takes care of returning that single record of Settings, checking if that record exists and creating it if it doesn't (first time running app basically)
Ideally, I'd like to keep the functionality inside the extension of Ent_Settings, but my problem is I can't get the correct instance of the moc to persistently save it.
#main
struct MyApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
MainMenuView().onAppear(perform: {
if Ent_Lists.hasAnyList(persistenceController.container.viewContext) == false {
Ent_Lists.createAnyList(persistenceController.container.viewContext)
}
if Ent_Section.hasBlankSection(persistenceController.container.viewContext) == false {
Ent_Section.createBlankSection(persistenceController.container.viewContext)
}
//self.settings = Ent_Settings.returnSettings(persistenceController.container.viewContext)
})
//ContentView()
//.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
I'm pretty certain that let persistenceController = PersistenceController.shared is initializing the persistent controller used throughout the app. Either creating it or retrieving it.
So, I tried this first inside of Ent_Settings:
let moc = PersistenceController.shared.container.viewContext
but I think that this might be creating a new PersistenceController outside the one made inside of MyApp
I also tried let moc = EZ_Lists_SwiftUI.PersistenceController.shared.container.viewContext but I'm pretty sure that also makes a new instance given I can only access the upper case PersistenceController and not the lower case one.
I also tried just passing the moc from the view like this: class func createSettingsEntity(_ moc: NSManagedObjectContext) { but I get an error about the moc being nil, so I'm guessing the moc can't be sent by reference from a Struct.
Thread 1: "+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Ent_Settings'"
And just to be clear, I'm adding this to the view: #Environment(\.managedObjectContext) private var viewContext adding this #State property to the View: #State private var settings: Ent_Settings! and setting it within .onAppear with this: self.settings = Ent_Settings.returnSettings(self.viewContext)
So what I'm really looking for is how I access the moc from within that extension of Ent_Settings. In my app that was purely UIKit and Storyboards I used this: let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext but as there is no longer an AppDelegate, I cannot use that anymore. What replaces this to get moc when you are doing SwiftUI but working outside of the Views, like in a class or something like I am? Thank you. Hopefully this extra information helps explain my issue.
If you are only using coredata to store settings, I would look into UserDefaults and if that matches your use case.
https://developer.apple.com/documentation/foundation/userdefaults
If you for some reason need to use CoreData, I would recommend enabling CloudKit, as CloudKit will allow you to actually view what data is saved via a web console.
Also, SwiftUI does not create an AppDelegate for you, you are still able to just create your own AppDelegate.swift file as you would with UIKit.
Just define the #UIApplicationDelegateAdaptor to your custom AppDelegate.swift file in your top most struct (the one with #main) before the struct declaration.
#main
struct MainAppView: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
Okay, a bit of egg on my face. I finally figured out why I was having so much trouble. When I was making my app, I commented out everything related to the apple provided ContextView and commented out too much.
//ContentView()
//.environment(\.managedObjectContext, persistenceController.container.viewContext)
I lost the part where the environment object was getting setup. After putting that back in I'm able to save to the moc now.
In my Entity class extension I'm accessing the moc this way:
let moc = MyApp.PersistenceController.shared.container.viewContext
Thank you for everyone who looked and tried to help. Turns out I was shooting myself in the foot.

Observe a string and get from API with RxSwift

I have a MVVM test project to experiment RxSwift. I have a UItextfield a button. User write a food name, click on the button and a get from an API is triggered to get all recipes with that food.
View model
struct FoodViewModel
var foodIdentifier: Variable<String> = Variable<String>("")
init() {
foodIdentifier.asObservable().subscribe(onNext: { (identifier) in
self.getRecipes() // Get from API
})
}
}
ViewController
class FoodViewController: UIViewController {
#IBOutlet weak var foodTextField: UITextField!
#IBAction func setCurrentRace(_ sender: Any) {
viewModel.foodIdentifier.value = foodTextField.text!
}
}
After compile I got an error
Closure cannot implicitly capture a mutating self parameter
What I'm doing wrong ? I think it's because of struct of FoodViewModel. If yes, how can I achieve that using struct ?
-- EDIT
I wrote all of the below but forgot to answer your explicit question... The reason you are getting the error is because you are trying to capture self in a closure where self is a struct. If this were allowed, you would be capturing a copy of the view model that you haven't even finished constructing. Switching your view model to a class alleviates the problem because you are no longer capturing a copy, but the object itself for later use.
Here is a better way to set up a view model. You didn't give all the necessary information so I took some liberties...
First we need a model. I don't know exactly what should be in a Recipe so you will have to fill it in.
struct Recipe { }
Next we have our view model. Note that it doesn't directly connect with anything in the UI or the server. This makes testing very easy.
protocol API {
func getRecipies(withFood: String) -> Observable<[Recipe]>
}
protocol FoodSource {
var foodText: Observable<String> { get }
}
struct FoodViewModel {
let recipes: Observable<[Recipe]>
init(api: API, source: FoodSource) {
recipes = source.foodText
.flatMapLatest({ api.getRecipies(withFood: $0) })
}
}
In real code, you aren't going to want to make a new server call every time the user types a letter. There are a lot of examples on the web that explain how to build in a delay that waits until the user stops typing before making the call.
Then you have the actual view controller. You didn't mention what you wanted to do with the results of the server call. Maybe you want to bind the result to a table view? I'm just printing the results here.
class FoodViewController: UIViewController, FoodSource {
#IBOutlet weak var foodTextField: UITextField!
var api: API!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = FoodViewModel(api: api, source: self)
viewModel.recipes.subscribe(onNext: {
print($0)
}).disposed(by: bag)
}
var foodText: Observable<String> {
return foodTextField.rx.text.map { $0 ?? "" }.asObservable()
}
let bag = DisposeBag()
}
Notice how we avoid having to make an IBAction. when you are coding up a view controller with Rx, you will find that almost all the code ends up in the viewDidLoad method. This is because with Rx, you are mainly just worried about wiring everything up. Once the observables are wired up, user action will cause things to happen. It's more like programming a spreadsheet. You just put in the formulas and link the observables together. User's data entry takes care of the actual action.
The above is just one way of setting everything up. This method matches closely with Srdan Rasic's model: http://rasic.info/a-different-take-on-mvvm-with-swift/
You could also turn the food view model into a pure function like this:
struct FoodSink {
let recipes: Observable<[Recipe]>
}
func foodViewModel(api: API, source: FoodSource) -> FoodSink {
let recipes = source.foodText
.flatMapLatest({ api.getRecipies(withFood: $0) })
return FoodSink(recipes: recipes)
}
One takeaway from this... Try to avoid using Subjects or Variables. Here's a great article that helps determine when using a Subject or Variable is appropriate: http://davesexton.com/blog/post/To-Use-Subject-Or-Not-To-Use-Subject.aspx