Why cant we reuse State Modifiers? - famo.us

I am just trying to understand famo.us and wondering why we cant reuse StateModifier instance when the values are the same.
var rotateModifierOne = new StateModifier({
transform: Transform.rotateZ(Math.PI/4)
});
var rotateModifierTwo = new StateModifier({
transform: Transform.rotateZ(Math.PI/4)
});
As seen here: http://famo.us/university/famous-101/positioning/4/
Why cant we just do
var mainContext = Engine.createContext();
var translateModifier = new StateModifier({
transform: Transform.translate(200, 0, 0)
});
var rotateModifier = new StateModifier({
transform: Transform.rotateZ(Math.PI/4)
});
var redSurface = new Surface({
size: [100, 100],
classes: ['red-bg']
});
var greySurface = new Surface({
size: [100, 100],
classes: ['grey-bg']
});
mainContext
.add(translateModifier)
.add(rotateModifier)
.add(redSurface);
mainContext
.add(rotateModifier)
.add(translateModifier)
.add(greySurface);
Why do we need separate instances when the values are the same? I am expecting the answer to help me understand how State Modifiers are consumed.

Seems like that should work and this drove me crazy when I started. I can't say I know what their reasoning behind making it not work is but as I'm sure you saw later in that same lesson they intend you add a modifier to the context or really anything your attaching to and making that a new variable that holds that offset/transform to let you do relative placement.
I can say their method is cleaner and easier to read than seeing the same modifier over and over again. Especially if your using this a lot to give relative based placement. Check this out http://famo.us/guides/render-tree . I also used to git really annoyed with Modifier vs StateModifier, took me awhile to find http://famo.us/guides/layout
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Transform = require('famous/core/Transform');
var StateModifier = require('famous/modifiers/StateModifier');
var mainContext = Engine.createContext();
mainContext.downMod = new StateModifier({
transform: Transform.translate(0, 100, 0)
});
var leftSurface = new Surface({
size: [120, 100],
content: 'left surface',
properties: {
color: 'white',
backgroundColor: '#FA5C4F'
}
});
var rightSurface = new Surface({
size: [120, 100],
content: 'right surface',
properties: {
color: 'white',
backgroundColor: '#404040'
}
});
rightSurface.rightMod = new StateModifier({
transform: Transform.translate(150, 0, 0)
});
var node = mainContext.add(mainContext.downMod);
//Now anything attached to node is actually attached to mainContext with that singular modifier attached
node.add(leftSurface);
node.add(rightSurface.rightMod).add(rightSurface);

StateModifier's are consumed the same as the Modifier object they extend. They are required to have a one to one relationship with their child node. StateModifiers change the state of their child renderable visual changes as stated in the comments.
The reason you need a separate modifier for each node is by design. There is a one to one relationship from a modifier to another node in the tree. This is due to the placement of each modifier (node) to have a parent child relationship.
The functional reason, for not applying a modifier to more than one Renderable at a time, may be for performance reasons and to keep code maintainable. There is a design pattern and best practice that is shown in the examples below to accomplish the answer to the question above. For our purposes, a StateModifier is not really anything more than a Modifier that maintains properties of it's children, so we will not explain the difference here.
A couple facts:
There are two types of nodes in the tree, Modifiers and Renderables
Each node represents a level of a branch in the tree
In your example you wanted to do the same effect on surface one and surface two:
Take this example first: Two nodes
mainContext.add(translateModifierOne).add(rotateModifierOne).add(redSurface);
mainContext.add(translateModifierTwo).add(rotateModifierTwo).add(greySurface);
It is similar to your example and has two modifier nodes doing the same exact thing by placing the surfaces in the same exact location as each other but rotating different directions with the rotate modifiers.
Now let's look at this example: One node
var myView = new RenderNode();
myView.add(rotateModifierOne).add(redSurface);
myView.add(rotateModifierTwo).add(greySurface);
mainContext.add(translateModifierTwo).add(myView);
It uses one modifier applied to a render node that places the two surfaces in the same exact place. The parent render node (myView) has two nodes attached at the same level and effects both of their placement. It does show how the one to one relationship of modifiers to nodes can be applied to more than one surface by using a render node.
Yet Another example: One node chaining another One
var myView = new RenderNode();
var initialTime = Date.now();
var myState = new Modifier({
transform: function(){
return Transform.rotateZ(0.002 * (Date.now() - initialTime));
}
});
myView.add(redSurface);
myView.add(greySurface);
mainContext.add(translateModifierTwo).add(myState).add(myView);
In this example, we see two modifiers chained together to transition a render node that has two surfaces added. In this case we are applying the same effect to both surfaces using their parent render node. It is hard to see, but they are stacked on top of each other in the same exact location. It kind of translates to how you would set up applying a modifier to two different surfaces. Not really useful, but did get rid of the duplicate modifiers.
Simple fix example: Give each surface it's own modifier
myView.add(rotateModifierOne).add(redSurface);
myView.add(rotateModifierTwo).add(greySurface);
We apply a parent modifier to each of the surfaces to give them their uniqueness in the tree to make their rotation offset by our original modifiers.

Related

SwiftUICharts not changing color on bar chart

I am trying to build a watchOS app that has charting and I can't use Swifts built in charts because I need to support down to version 7 and swift charts are only available for watchOS 9+. So instead I am using a library I found here...
https://github.com/willdale/SwiftUICharts
It has some sample and examples, trying to follow them I was able to get the chart to show up and customize it some, but I can't get the bar chart items to change their color. It seems simple enough, but for whatever reason I can't get it to actually change the color.
I'll provide a simple sample that should help show what's going on.
struct ChartView: View {
var items: [TimeEntries] = [
TimeEntry(dateString: "01/23/2023", entry: 97, timestamp: Date().millisecondsSince1970)]
var body: some View {
let chartData = makeData(items)
BarChart(chartData: chartData)
.touchOverlay(chartData: chartData)
.padding()
}
private func makeData(_ items: [TimeEntries]) -> BarChartData {
var data: [BarChartDataPoint] = [BarChartDataPoint]()
for item in items {
let stat = BarChartDataPoint(
value: Double(item.entry),
xAxisLabel: "Wed",
date: Date(milliseconds: entry.timestamp),
colour: ColourStyle(colour: Color.purple)
)
data.append(stat)
}
let dataSet = BarDataSet(dataPoints: data)
return BarChartData(dataSets: dataSet)
}
}
That should give me an entry on my bar chart with purple filling, I simplified this for sake of ease of posting, my real data has 7 points in it.
However, what actually happens is I have a single red bar on my chart. I am not using red anywhere in the app at all, but it won't take the color that I specify in the colour property of the BarChartDataPoint.
I know it's based on a library, but hopefully someone here will have used this library and will know what I have done wrong. I'll attach a screenshot of the chart so you can see. Thank you.

How can I update/save a Property Change for Child Item from a Hierarchical List View of Items

See the following app screens:
Content View Screen:
Content View with hierarchical list children rows disclosed:
Parent Row Detail View:
Child Row Detail View:
Referencing the above views, here are the steps I do and the resulting problem I’m trying to solve:
Launch the app.
From the Functions (Content View) presented at launch, see that there is one item listed in a list view (1.0 Move Vessel)
Click the yellow (my app accent color) disclosure arrow at the right of the list item.
Two subordinate child list rows appear under the parent list item, 1.1 Move Position and 1.2 Hold Position.
When I tap the parent item (1.0 Move Vessel) in the hierarchy list, I'm successfully able to navigate to a detail view for that tapped item.
Edit the description of the 1.0 Move Vessel item (defaults to test) of the tapped item properties in the detail view using a TextEditor view.
Click yellow Save button at top left of detail view. The app navigates back to the parent Functions (Content View).
Click on the parent 1.0 Move Vessel row again.
See that description was successfully saved and now displayed from the change made in Step 5 and 6.
Repeat steps 5 through 8 again for 1.1 Move Position list row.
See that the edit/change made to the description was not saved and the default test1 description is displayed instead (not what is wanted).
Repeat steps 5 through 8 again for 1.2 Hold Position list row.
See that the edit/change made to the description was not saved and the default test2 description is displayed instead (not what is wanted).
I think I may have a problem in my save code logic and I'm trying to investigate.
Here are the swift files for the Detail View, the View Model, and the Model (I’ve not included the content view code because that code is working ok with the detail view. Again, I think the problem is in my save button and function call code for updating the view model.
NOTE: sorry that I can’t seem to figure out how to get all the code for a file contiguous in the code view. I seem to have some closing braces that don’t appear in the code view. I think you can still follow the code.
struct FunctionDetailView: View {
#State var vesselFunction: VesselFunction
#State var vesselFunctionDescription: String
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var functionViewModel : FunctionViewModel
var body: some View {
NavigationView {
Form {
Text("Enter description below")
TextEditor(text: $vesselFunctionDescription)
.frame(height: 200)
.toolbar {
Button {
//print(vesselFunction)
vesselFunction.funcDescription = vesselFunctionDescription
//print(vesselFunction)
functionViewModel.updateVesselFunction(vesselFunction: vesselFunction)
//print(vesselFunction)
presentationMode.wrappedValue.dismiss()
} label: {
Text("Save")
}
}
}
.padding()
.navigationTitle(vesselFunction.name)
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct FunctionDetailView_Previews: PreviewProvider {
static var previews: some View {
FunctionDetailView(vesselFunction: VesselFunction(id: UUID(), name: "x.x Verb Noun", funcDescription: "Description", children: nil), vesselFunctionDescription: "placeholder")
.environmentObject(FunctionViewModel())
.preferredColorScheme(.dark)
}
}
FunctionViewModel.swift
#MainActor class FunctionViewModel: ObservableObject {
#Published private(set) var decomp : [VesselFunction] = [
VesselFunction(id: UUID(), name: "1.0 Move Vessel", funcDescription: "test", children: [
VesselFunction(id: UUID(), name: "1.1 Move Position", funcDescription: "test1", children: nil),
VesselFunction(id: UUID(), name: "1.2 Hold Position", funcDescription: "test2", children: nil)
])
]
func updateVesselFunction(vesselFunction: VesselFunction) {
/*
if let index = decomp.firstIndex(where: { (existingVesselFunction) -> Bool in
return existingVesselFunction.id == vesselFunction.id
}) {
//run this code
}
*/
// cleaner version of above
if let index = decomp.firstIndex(where: { $0.id == vesselFunction.id }) {
decomp[index] = vesselFunction.updateCompletion()
}
/*
else {
for item in decomp {
if item.children != nil {
if let index = item.children?.firstIndex(where: { $0.id == vesselFunction.id }) {
item.children![index] = vesselFunction.updateCompletion()
}
}
}
} */
}
}
FunctionModel.swift
struct VesselFunction: Identifiable {
let id : UUID
let name : String
var funcDescription : String
var children : [VesselFunction]?
init(id: UUID, name: String, funcDescription: String, children: [VesselFunction]?) {
self.id = id
self.name = name
self.funcDescription = funcDescription
self.children = children
}
func updateCompletion() -> VesselFunction {
return VesselFunction(id: id, name: name, funcDescription: funcDescription, children: children)
}
}
As you can see from the else and for-in loop code commented out at the bottom of the FunctionViewModel code, I was trying to see if I needed to do something like this code to access the children VesselFunction array entries of the decomp published property. With the if let index code that is not commented out, the save function works but only for the top-level decomp array VesselFunction elements, not the nested children arrays elements.
Any help would be appreciated so all decomp array elements, both parent and nested children, can be updated when the TextEditor field is changed and the Save button is pressed in the FunctionDetailView.
NOTE: I am only showing a 1 level deep nested array of children for the decomp property. I actually want to have multiple (at least 3) level of children arrays, so if you have any ideas how to make an updateVesselFunction function work for multiple children array elements, I would appreciate it.
In the main View use ForEach($model.items) { $item in so you get a write access to the model item. In the detail View change the #State to #Binding.
The issue isn't so much your code right now, as it is the architecture of the program. You really need to reorganize the app with MVVM concepts in mind. If you are not sure of them, study Apple’s SwiftUI Tutorials & Stanford’s CS193P. Without a proper architecture, you have gotten lost down a rabbit hole to the extent that I gave up trying to fix the code.
Also, given the structure of your data, I would give serious consideration to using Core Data to model it. Your VesselFunction struct contains an array of VesselFunction, and that it much better modeled as a relationship, rather than having a struct hold an array of the same struct which can hold an array of the same struct. It is a nightmare to deal with as a struct, instead of as a Core Data class.
I would also consider make your FunctionDetailView just display data, and have a separate editing view. This will keep your view separate and easier to manage.
Lastly, you have a lot of redundancy in your naming conventions. Theoretically, you could be trying to access a piece of data at functionViewModel.funcDescription (Not to mention: functionViewModel.children[index].children[subIndex].children[subSubIndex].funcDescription); this can get a bit unwieldy. The further you go down, the worse it will get.

SwiftUI View not updating on async change to published properties of Observed Object

I have the following SwiftUI View:
struct ProductView: View {
#ObservedObject var productViewModel: ProductViewModel
var body: some View {
VStack {
ZStack(alignment: .top) {
if(self.productViewModel.product != nil) {
URLImage(url: self.productViewModel.product!.imageurl, itemColor: self.productViewModel.selectedColor)
}
else {
Image("loading")
}
}
}
}
that observes a ProductViewModel
class ProductViewModel: ObservableObject {
#Published var selectedColor: UIColor = .white
#Published var product: Product?
private var cancellable: AnyCancellable!
init(productFuture: Future<Product, Never>) {
self.cancellable = productFuture.sink(receiveCompletion: { comp in
print(comp)
}, receiveValue: { product in
self.product = product
print(self.product) // this prints the expected product. The network call works just fine
})
}
The Product is a Swift struct that contains several string properties:
struct Product {
let id: String
let imageurl: String
let price: String
}
It is fetched from a remote API. The service that does the fetching returns a Combine future and passes it to the view model like so:
let productFuture = retrieveProduct(productID: "1")
let productVM = ProductViewModel(productFuture: productFuture)
let productView = ProductView(productViewModel: productViewModel)
func retrieveProduct(productID: String) -> Future<Product, Never>{
let future = Future<Product, Never> { promise in
// networking logic that fetches the remote product, once it finishes the success callback is invoked
promise(.success(product))
}
return future
}
For the sake of brevity, I've excluded the networking and error handling logic since it is irrelevant for the case at hand. To reproduce this as quickly as possible, just initialize a mock product with some dummy values and pass it to the success callback with a delay like this:
let mockproduct = Product(id: "1", imageurl: "https://exampleurl.com", price: "$10")
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: {
promise(.success(mockproduct))
})
Once the product arrives over the network, it is assigned to the published product property.
The fetching works and the correct value is assigned to the published property. Obviously this happens after the view has been created since the network call takes some time. However, the View never updates even though the published object is changed.
When I pass the product directly through the View Model initializer rather than the future, it works as expected and the view displays the correct product.
Any suggestions on why the view does not react to changes in the state of the view model when it is updated asynchronously through the combine future?
EDIT: When I asked this question I had the ProductViewModel + ProductView nested inside another view. So basically the productview was only a part of a larger CategoryView. The CategoryViewmodel initialized both the ProductViewModel and the ProductView in a dedicated method:
func createProductView() -> AnyView {
let productVM = productViewModels[productIndex]
return AnyView(ProductView(productViewModel: productVM))
}
which was then called by the CategoryView on every update. I guess this got the Published variables in the nested ProductViewModel to not update correctly because the view hierarchy from CategoryView downwards got rebuilt on every update. Accordingly, the method createProductView got invoked on every new update, resulting in a completely new initialization of the ProductView + ProductViewModel.
Maybe someone with more experience with SwiftUI can comment on this.
Is it generally a bad idea to have nested observable objects in nested views or is there a way to make this work that is not an antipattern?
If not, how do you usually solve this problem when you have nested views that each have their own states?
I have been iterating on patterns like this to find what I think works best. Not sure what the problem is exactly. My intuition suggests that SwiftUI is having trouble making updates on the != nil part.
Here is the pattern that I have been using which has been working.
Define an enum for states in your networking logic
public enum NetworkingModelViewState {
case loading
case hasData
case noResults
case error
}
Add the enumeration as a variable on your "View Model"
class ProductViewModel: ObservableObject {
#Published public var state: NetworkingModelViewState = .loading
}
Update the state as you progress through your networking
self.cancellable = productFuture.sink(receiveCompletion: { comp in
print(comp)
}, receiveValue: { product in
self.product = product
self.state = NetworkingModelViewState.hasData
print(self.product) // this prints the expected product. The network call works just fine
})
Now make a decision in your SwiftUI based on the Enum value
if(self.productViewModel.state == NetworkingModelViewState.hasData) {
URLImage(url: self.productViewModel.product!.imageurl, itemColor: self.productViewModel.selectedColor)
}
else {
Image("loading")
}
Musings ~ It's hard to debug declarative frameworks. They are powerful and we should keep learning them but be aware of getting stuck. Moving too SwiftUI has forced me to really think about MVVM. My takeaway is that you really need to separate out every possible variable that controls your UI. You should not rely on checks outside of reading a variable. The Combine future pattern has a memory leak that Apple will fix next release. Also, you will be able to switch inside SwiftUI next release.

how to set alt or title tag for ImageSurface?

The image generated using the following example Famo.us example: ImageSurface generates an <img> tag without alt or title attributes.
Is there a built-in function that allows adding HTML attributes?
There are currently no built in functions but by looking at the source code I figured out a way to do such a thing. Keep in mind though, I think the better alternative would be to just use a Surface and inject and img tag with all the attributes you want into the content attribute..
Here is what I had to do using the ImageSurface. Note that the function is in a Timeout, and this is only because the image tag needed time to load for me to access it via the Surface..
var Engine = require("famous/core/Engine");
var Modifier = require("famous/core/Modifier");
var ImageSurface = require("famous/surfaces/ImageSurface");
var Timer = require("famous/utilities/Timer")
var mainCtx = Engine.createContext();
var image = new ImageSurface({
size: [200, 200],
});
image.setContent("content/famous_symbol.svg");
mainCtx.add(new Modifier({origin: [.5, .5]})).add(image);
add_title_alt = function(){
if (image._currTarget) {
image._currTarget.alt = "my-alt";
image._currTarget.title = "my-title";
} else {
Timer.setTimeout(add_title_alt,100);
}
}
Timer.setTimeout(add_title_alt,100);
Again though, to make things less sticky.. I would just do something like this..
image = new Surface({
content:"<img alt='my-alt' title='my-title' src='content/famous_symbol.svg' />"
})
Hope this helps!

How to remove events from nicEditor + emberjs

I am working on an Ember.js - based platform, where I use nicEdit. Here is my code
RichHTMLView = Ember.TextArea.extend({
id: null,
editor: null,
didInsertElement: function(){
var view = this;
view.id = this.get("elementId");
view.editor = new nicEditor({
buttonList : ['bold','italic','underline','right','center','justify', 'link', 'ul', 'ol']
}).panelInstance(view.id);
//When the editor looses focus the content of the editor is passed to descr
view.editor.addEvent('blur',function(){
view.get('controller').set('descr',view.getViewContent());
});
//So the editor looks nice
$('.nicEdit-panelContain').parent().width('100%');
$('.nicEdit-panelContain').parent().next().width('100%');
},
getViewContent: function(){
var view = this,
inlineEditor = view.editor.instanceById(view.id);
return inlineEditor.getContent();
},
willClearRender: function(){
var view = this;
}
});
So this works nicely as long as I am on the page which hosts the view, but if I transition to another route, the view has some leftovers, namely the editor is destroyed, but I assume that nicEdit keeps track of event bindings, so I end up with the blur event being bound to editor, which is undefined in the new context, as the view does not exist.
My best guess is that I need to somehow unbind the editor in the willClearRender, but I don't know how.
as I got no reply and nicEdit is abandoned I made some changes to the source-code in order to deal with this issue by adding removeEvent to bkEvent:
removeEvent: function(A, B){
if (B){
this.eventList = this.eventList || {};
this.eventList[A] = this.eventList[A] || [];
this.eventList[A].splice(this.eventList[A].indexOf(B),1);
}
Then I can remove the event in willClearRender:
view.editor.removeEvent('blur',view.onBlur);
Be aware that I've not tested it with multiple editors, as my needs do not require, but if you have multiple editors with the same callback the behavior is not defined.