RxSwift MVVM tableview/collectionview with user input cells representing state - rx-swift

I have one request for any of you. I want to create collectionview/tableview which will have user inputs in cells. Mixture of values from those inputs would represent state. I want to observe that state and if some conditions are met, I want to recreate all cells. I have created very simple app, where I demonstrate how i tried to implement it, but I’m getting reentrancy warning and I would love to find out your ideas/best practicies for this. Here is the repo you can checkout.
https://github.com/beretis/CollectionViewTest
PS: Im using RxData sources, and I would love to know exactly what is causing this reentrancy (I have my idea)

I sent a pull request your way.
The key to answer this question is to have two Observables in your view model. One that represents the programatic state of each cell (the stuff that the user doesn't input) and one that represents the user input state of each cell. You connect the data from these two Observables using some sort of ID value (I use UUID.) So for your specific example, the view model for the collection should look like this:
typealias CellID = UUID
struct StaticCellState {
let id: CellID
let placeholder: String
}
struct CollectionViewModel {
let cells: Observable<[StaticCellState]>
let cellStates: Observable<[CellID: String]>
}
The cells observable contains the placeholder and cell ID. This is the data that the cell uses when it is configured and doesn't change for the life of that configuration (It might change if the cell is reused.) It is only updated if you want to add/remove a cell or change the placeholder value of a particular cell.
The cellStates observable contains the latest user input values and is updated every time the user types into one of the cells' text fields.
Then you configure your cells by passing in the information for that cell from both observables:
let dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfCustomData>(
configureCell: { dataSource, collectionView, indexPath, item in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? SomeCell else { return UICollectionViewCell() }
let output = cell.configure(with: item, initial: viewModel.cellStates.map { $0[item.id]! })
output
.bind(to: itemEdit)
.disposed(by: cell.disposeBag)
return cell
})

Related

I want to addChild the Entity created by the RealityComposer

I'm adding addChild to a custom class that inherits the Entity that I created with RealityComposer, but the Entity is not placed at the tapped position and is displayed in the center of the screen.
I'm using the official sample by Apple of collaborative session creation and implemented so far I've done it. (It doesn't use RealityComposer.)
For example, tapping on an Entity will place it in that location.
However, when I add an Entity to the scene, such as a container that is an addChild of an Entity created by RealityComposer, it always appears in the middle.
My guess is that this is because Entities created with the RealityComposer are not HasModel compliant.
In this code, the Entity is always in the center of the screen
(I've already created a QRScene.rcproject.)
final class QRCardEntity: Entity, HasModel {
let twitterCard = try! QRScene.loadTwitterCard ()
var cardContainer: HasModel {
twitterCard.allChildren().first { $0.name == " CardContainer" }! .children[0] as! HasModel
}
required init() {
super.init()
addChild(twitterCard) // preservingWorldTransform: true does not change this.
}
}
However, this code puts it in the right place.
final class QRCardEntity: Entity, HasModel {
let twitterCard = try! QRScene.loadTwitterCard ()
var cardContainer: HasModel {
twitterCard.allChildren().first { $0.name == "CardContainer" }! .children[0] as! HasModel
}
required init() {
super.init()
model = cardContainer.model
}
}
Extensions used
private extension Entity {
func allChildren() -> [Entity] {
children.reduce([]) { $0 + [$1] + $1.allChildren () }
}
}
I don't think this is the best way.
AddChild is a better way to add a whole Entity while preserving the hierarchical relationship.
This model assignment can only add models from the Top hierarchy, so it doesn't add the other Models needed for display.
How do I get the Entity created by the RealityComposer to appear in the correct position with addChild?
ps 2020/07/03
To be precise, when you tap on Device1, Entity appears in the center and
Device2 shows Entity asynchronously centered no matter where the camera is pointed.
Instead of Entity of created by RealityComposer, using code like this, it works.
addChild(
ModelEntity(
mesh: .generateBox(size: 0.1),
materials: [
SimpleMaterial(color: color ?? .white, isMetallic: false)
]
)
)
If the position is already zero, you could try something like this that may get it looking right, but the positions of your Entities may be a bit back and forth:
twitterCard.position = -twitterCard.visualBounds(relativeTo: nil).center
That may need a little tweaking, but hopefully the intention is clear.
I've had issues understanding the hierarchy that Reality Composer gives to Entities, which is why I have avoided using it.
I solved by myself.
The reason is because I added the scene as child.
This code is to load the scene created by RealityComposer.
TwitterCard was scene name.
let twitterCard = try! QRScene.loadTwitterCard()
Scene name is presented in here.
So, correctly ModelEntity's name is here.
I needed to use this.
This load correctly.
addChild(twitterCard.cardObject!)
Only write this, it could add child Entity created by RealityComposer.
I have problem yet.
I can’t post notification for moving motion created by RealityComposer.
Because of twitterCard is not added to ARView’s scene. If can’t do this, we must write a lot of animation codes.
When needed, I create another post, thanks.

spfx - onpropertychange event

I have created cascading dropdown. I need to load dropdown based on parent dropdown selection. I am trying to use onpropertychange event. but I am getting error on super.onpropertychange saying {Property 'onPropertyChange' does not exist on type 'BaseClientSideWebPart'.}
please let us know what I havve missed.
protected onPropertyChange(propertyPath: string, newValue: any):void{
if(propertyPath === "listDropDown"){
// Change only when drop down changes
super.onPropertyChange(propertyPath,newValue);
// Clears the existing data
this.properties.ItemsDropDown = undefined;
this.onPropertyChange('ItemsDropDown', this.properties.ItemsDropDown);
// Get/Load new items data
this.GetItems();
}
else {
// Render the property field
super.onPropertyChange(propertyPath, newValue);
}
}
Instead of onPropertyChange, perhaps you mean onPropertyFieldChanged from the BaseWebPart class?
The error message is accurate - web parts don't have a method called onPropertyChange. The above sounds like the closest match for what you are trying to do. Note that it takes not two arguments, but three: propertyPath, oldValue, and newValue.

When is a Cocoa binding 'set'

I've got an NSArrayController set in my storyboard where I set the mode to Entity Name with a name of Client, and bound the managed object context, selection indexes, and sort descriptors. My NSPopupButton links to that array controller and when I run I see all the elements I expect on the button.
Now I made a strong #IBOutlet in my code and I'm trying to access the contents:
let objs = clientArrayController.arrangedObjects as! [Client]
print("I have \(objs.count) clients")
I tried that code in viewDidLoad, viewWillAppear and viewDidAppear. They all say 0 clients. Clearly that's not possible as I have the clients showing in the UI.
What am I doing wrong here?
viewDidLoad, viewWillAppear, and viewDidAppear are possibly all called before your array controller collects its data from the core data store - hence they (correctly) report a count of zero. To get word of any changes to your array controller's arrangedObjects array you could use one of the aforementioned methods to install an observer that watches this object and reports any changes:
// MyPopUpController.swift
var ArrayControllerArrangedObjectsObservationContext = "arrayController.arrangedObjects"
func viewDidLoad() {
arrayController.addObserver(self,
forKeyPath: "arrangedObjects",
options: .New | .Old,
context: &ArrayControllerArrangedObjectsObservationContext)
}
You're given the opportunity to respond to changes in observeValueForKeyPath...
// MyPopUpController.swift
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
switch context {
case &ArrayControllerArrangedObjectsObservationContext:
// Check counts here
default:
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
If you only need the observer on start-up, after you've checked the counts, and done whatever you need to do you should then remove the observer:
// MyPopUpController.swift
arrayController.removeObserver(self,
forKeyPath: "arrangedObjects",
context: &ArrayControllerArrangedObjectsObservationContext)

Core Data Problems?

I am trying to use CoreData but I already went through the setup for my project and forgot to check off the box to utilize it.
Is there a way to implement the use of core data when the CoreData box was not checked previously during the setup?
If I start a new project will have to transfer a lot of information and it would be time consuming so I would like to stay on the same project and not create a new one.
To be frank, you did the right think by not checking the Use CoreData box on project creation. I feel that just bloats the project with a bunch of stuff that is easier (and more insightful) to do manually.
To be short, you can implement CoreData the same regardless of what option you selected at project creation.
Here are the steps I usually go through when I want to add CoreData support to my project (manually/programatically):
Define a Data Model
These are just NSManagedObjects which represent your application's data structure. For example, a User, Message, BlogPost, etc. I also make one for my user settings.
Example:
import CoreData
class User : NSManagedObject
{
// #NSManaged is the replacement for #dynamic when using CoreData in Swift
#NSManaged var identifier : String
#NSManaged var firstName : String?
#NSManaged var lastName : String?
// This is called when a new User object is inserted to CoreData
override func awakeFromInsert()
{
super.awakeFromInsert()
self.identifier = NSUUID().UUIDString // generate a random unique ID
}
}
Add Core Data Model
This is another file you add to your project via: File -> New-> iOS-> CoreData -> Data Model. I usually store this same xcmodeldata file in my Models project folder (along with my actual model classes).
Upon selecting this new file, you'll see the CoreData model editor. You will want to see the right-hand side inspector pane is visible (hotkey is ⌥⌘1). For the core data editor, you will also primarily use the third tab (data model inspector) which is switchable with ⌥⌘3.
Now you can add an entity object to this data model (via Add Entity at the bottom). Assuming the example above, add a User entity. With the User entity selected, add the three attributes that are defined in the above class: identifier, firstName, and lastName. They should match the class definition, using String types.
Next step is to tell CoreData that this User entity defined here maps to our actual class file. With the User selected and the data model inspector pane open, set the Name to User and Class to YourAppName.User.
This is the "gotcha" with Swift and CoreData, your classes are prefixed with the module name in order to namespace them (avoiding name collisions). The nice part is that you no longer need to add "XYZ" class prefixes to your objects.
Initialize Core Data Stack
With your data model defined, you need to initialize the CoreData stack itself (database store and context). The most basic example is a global singleton for your NSManagedObjectContext, which will be lazy-loaded when needed.
You can put this in its own Swift file (CoreDataStack.swift):
import CoreData
let managedObjectContext : NSManagedObjectContext =
{
// This is your xcdatamodeld file
let modelURL = NSBundle.mainBundle().URLForResource("MyApp", withExtension: "momd")
let dataModel = NSManagedObjectModel(contentsOfURL: modelURL!)
// This is where you are storing your SQLite database file
let documentsDirectory : NSURL! = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).last as? NSURL
let storeURL = documentsDirectory.URLByAppendingPathComponent("MyApp.sqlite")
let psc = NSPersistentStoreCoordinator(managedObjectModel: dataModel!)
var error : NSError?
let store = psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil, error: &error)
if let error = error
{
println("Uhoh, something happened! \(error), \(error.userInfo)")
}
let context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
context.persistentStoreCoordinator = psc
context.undoManager = nil
return context
}()
Using Core Data
So now that you have a working Core Data stack, some data models defined and mapped... what next?
Let's fetch some objects!
func getUsersByFirstName(firstName: String) -> [User]
{
let fetchRequest = NSFetchRequest(entityName: "User")
// The [c] part indicates case-insensitive, "Bob" == "bob"
fetchRequest.predicate = NSPredicate(format: "firstName ==[c] %#", firstName)
var error : NSError?
let results = context.executeFetchRequest(fetchRequest, error: &error) as [User]
// Handle errors here
return results
}
Oh right, we have nothing to fetch. You can also insert objects...
func insertNewUser() -> User
{
return NSEntityDescription.insertNewObjectForEntityForName("User", inManagedObjectContext: context) as User
}
And of course you can delete objects...
func deleteUser(user: User)
{
context.deleteObject(user)
}
The key is to remember that CoreData contexts (NSManagedObjectContext) keep track of changes in memory. While you can perform these CRUD operations on a context and see the changes instantly (within the same context), they will not persist in the database until you save the changes:
func saveContext() -> Bool
{
var error : NSError?
if context.hasChanges && !context.save(&error)
{
println("Something happened when saving! \(error!), \(error!.userInfo)")
return false
}
return true
}
You can also rollback changes from the last save by using context.rollback().
Feel free to explore CoreData and experiment with the more advanced features like predicates (NSPredicate), sort descriptors (NSSortDescriptor), and setting up object relationships.
Basically all the tick box for Core Data does is add the core data framework (CoreData.framework) to your project and setup your AppDelegate.m with the core data stack, add in a data file and possibly give you a sample view controller (depending on which project type you start with).
If you want your existing project to be setup like the template would set you up, then the quickest way is to just create a new project as an example and select Core Data tick box. Open the new project and review the AppDelegate.m file and grab the code for initializing core data stack. It's about 80 lines and has a comment calling out the Core Data Stack.
Take that over to your existing project and drop it in to your AppDelegate file. Also in your existing project, add the CoreData.framework, then add in a new file (File->New File->CoreData), under Core Data called a "Data Model" file. This file is used to define the equivalent of your data schema. You select it to use the graphical controls.
Then use your sample project to review how you access the core data stack by reviewing the sample ViewController.
Note: some people are not fond of how Apple sets up the Core Data stack in the AppDelegate.m and you'll find many comments about it and how to do it better, if you search for it (I feel compelled to make this disclaimer). There are also some 3rd party libraries on GitHub that can assist you in that regard as well. (MagicalRecord,SLCoreDataStack, etc).
hope that helps!
be well!

An NSArrayController changes its selection : what is the best way to catch this event?

One can put an observer on the selectedIndex method of NSArrayController. This method has some drawbacks I think :
what will happen when the arrangedObjects is rearranged ? I admit this is not a very important problem
if we ask the observer to remember the old value of selectedIndex, it doesn't work. It is known but I cannot find again the link.
Why doesn't NSArrayController have a delegate ?
Is there another way to achieve what I want to do : launching some methods when the selection changes ?
Observe selection key of the NSArrayController (it is inherited from NSObjectController).
It will return either NSMultipleValuesMarker (when many objects are selected), NSNoSelectionMarker (when nothing is selected), or a proxy representing the selected object which can then be queried for the original object value through self key.
It will not change if rearranging objects did not actually change the selection.
You can also observe selectedObjects; in that case you won't need to deal with markers.
Providing hamstergene's excellent solution, in Swift 4.
In viewDidLoad, observe the key path.
arrayController.addObserver(self, forKeyPath: "selectedObjects", options: .new, context: nil)
In the view controller,
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else { return }
switch keyPath {
case "selectedObjects":
// arrayController.selectedObjects has changed
default:
break
}
}

Resources