SwiftUI: Why does ForEach need an ID? - swiftui

Im using a ForEach loop in my SwiftUI View and I am getting strange warnings.
It works fine like this:
ForEach(0..<7) { i in
// do something
}
Then I changed 7 to a constant:
let numberOfElements = 7
ForEach(0..<numberOfElements) { i in
// do something
}
And got the following warning:
Non-constant range: argument must be an integer literal
I googled an found the following solution which works:
let numberOfElements = 7
ForEach(0..<numberOfElements, id:\.self) { i in
// do something
}
However, I have no idea why it works. Why do I have to give an ID to the ForEach loop, and what is the ID for?

ForEach(0..<numberOfElements) { i in
// do something
}
The reason why using the above ForEach init pops the using literal value warning is because SwiftUI doesn't expect to re-render anything when using the Range<Int> init method. This is a documented requirement / feature. The exceptions are the init methods with id:.
A hashable id matters in SwiftUI as well as in many other view-tree based frameworks like React is because these UI frameworks needs to use these ids to track updates for views inside the ForEach or any other "Container Views", to help the framework achieve usable performance. If you want to dig deeper, take a look at this WWDC video: Demystify SwiftUI.

Related

webview_flutter can't hide/change some elements

Using the webview_flutter with evaluateJavascript(), I've been able to modify the style of most elements of my website, but can't understand why some elements don't get modified.
My WebView is included, and the website to display within the WebView is https://dme.com.sg/index.php?dispatch=auth.login_form
I've included a snapshot as well, showing how I can hide and modify the colours for most of the elements, yet it seems all those that are within the "form" cannot be changed.
Would appreciate any help on how I can modify those as well, especially to change their colours to a darker theme to match the colours of the app.
WebView(
initialUrl: 'https://dme.com.sg/index.php?dispatch=auth.login_form',
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (controller) {
_controller = controller;
},
onPageStarted: (url) {
_controller.evaluateJavascript(
"document.getElementsByClassName('tygh-top-panel clearfix')[0].style.display='none';"
"document.getElementsByClassName('tygh-header clearfix')[0].style.display='none';"
"document.getElementsByClassName('tygh-header')[0].style.display='none';"
"document.getElementsByClassName('tygh-footer')[0].style.display='none';"
"document.getElementsByClassName('auth-information-grid')[0].style.display='none';"
"document.getElementsByClassName('ty-breadcrumbs clearfix')[0].style.display = 'none';"
"document.getElementsByClassName('container-fluid content-grid')[0].style.background = 'black';"
"document.getElementsByClassName('ty-mainbox-title')[0].style.color = 'pink';"
"document.getElementsByClassName('buttons-container clearfix')[0].style.display = 'none';"
);
},
),
Have kept tinkering around with it, and looking up JavaScript tutorial, and I managed to find a solution. So I'm not sure HOW or WHY, so still hoping someone could comment a response that explains so I can better understand.
Something else I've learnt, it's easier to just use the "console" tab of chrome to test the java scripts on the page before moving it into flutter webview.
Using the "document.getElementsByClassName("buttons-container clearfix")" command, I got a list of where the class was being used, and found the one I wanted to change was 1. It helps that when as you type the right index number, it gets highlighted on the website.
So then using "document.getElementsByClassName("buttons-container clearfix")1.style.background = 'black'" I managed to change the style of the element I wanted.
I've included a screengrab of the chrome console here if it can help anyone else.

How to use Identifiable in a ForEach loop

I am trying to iterate through an array of objects. This object is conforming to the Identifiable protocol. When using a ForEach loop, I get the following error: Type of expression is ambiguous without more context
I've included the block of code that is throwing the error. The error is specifically underlining \.name. Am I missing something?
Another note: This code worked in Xcode 11 Beta 2 but broke in Xcode 11 Beta 3...
struct ItemRow : View {
var categoryName:String
var items:[Item]
var body: some View {
VStack {
Text(self.categoryName)
.font(.title)
ScrollView(showsHorizontalIndicator: false) {
HStack (alignment: .top){
ForEach (self.items.identified(by: \.name)) { item in
NavigationLink(destination: ItemDetail(item: item)) {
ItemView(item: item)
.frame(width:300)
.padding(.trailing, 30)
}
}
}
}
}
}
}
Here is the Identifiable Object:
struct Item:Hashable, Codable, Identifiable {
var id:Int
var name:String
var category:Category
var description:String
}
(This code has been abstracted)
The first thing you need to know, is that when building views, compile errors can be very misleading. An error may show at the bottom of your code, but the cause may be at the top. I expect this will be fixed sometime in the future, but for the time being, you need to be careful.
Your code compiles just fine. Because of what I said about misleading errors, one brute but effective technique to debug the problem, is to start commenting bits of code until the error goes away. This will let pin point where the root of the problem may be.
A good way of updating your question is including enough code, so that people can reproduce the problem just by copy and paste into their own Xcode. It may be a lot of work for you, but I found that most of the time, you understand the problem during that process and you may not even need to post the question in the first place. Reducing an issue to its minimum expression, is also a great way of understading/fixing a problem.
UPDATE
Since you added more code, the error is showing where you would have not expected:
The ScrollView initialiser you were using, was deprecated. It now looks like this:
ScrollView(.horizontal, showsIndicators: false)
Also something that may potentially be a problem. You are using:
self.items.identified(by: \.name)
But don't you mean:
self.items.identified(by: \.id)
If so, then you do not need to use identified, since Item is already Identifiable and as such, it is already identified by id.
self.items
The issue was actually with the following line:
ScrollView(showsHorizontalIndicator: false)
ScrollView doesn't work like that anymore in Beta 3. The arguments now look something like this:
ScrollView(.horizontal, showsIndicators: false)
That will give you a horizontal scrolling view and will not show the scrolling indicators.

Xcode simulator not calling traitCollectionDidChange

I am overriding traitCollectionDidChange(_) to update my compact and regular constraints. When I test this on a device by rotating the constraints get updated correctly. When I however try to test the same code in the simulator nothing happens. I interjected print statements and I can see that on simulator rotation nothing happens. Is this a bug, or do I need to do something special for the simulator?
Thanks in advance. I am using Xcode 8.2.1 btw.
This is my code:
private var compactConstraints: [NSLayoutConstraint] = []
private var regularConstraints: [NSLayoutConstraint] = []
private var sharedConstraints: [NSLayoutConstraint] = []
...
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if (!sharedConstraints[0].isActive) {
// activating shared constraints
NSLayoutConstraint.activate(sharedConstraints)
}
if (self.traitCollection.containsTraits(in: UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClass.compact))) {
print("going to activate: compact")
if regularConstraints.count > 0 && regularConstraints[0].isActive {
NSLayoutConstraint.deactivate(regularConstraints)
}
// activating compact constraints
NSLayoutConstraint.activate(compactConstraints)
} else {
print("going to activate: regular")
if compactConstraints.count > 0 && compactConstraints[0].isActive {
NSLayoutConstraint.deactivate(compactConstraints)
}
// activating regular constraints
NSLayoutConstraint.activate(regularConstraints)
}
}
The console output is as follows:
[launch app]
`going to activate: compact`
[rotate the simulator with ⌘ arrow key]
`going to activate: compact`
Small update based on hoshy's question:
I am using the simulator with iPhone devices. Specifically iPhone SE.
horizontalSizeClass is Compact for both orientations. verticalSizeClass is regular for portrait and Compact for landscape for iPhone SE. You can change this line
if (traitCollection.containsTraits(in: UITraitCollection(verticalSizeClass: .compact))) {
or simply
if traitCollection.verticalSizeClass == .compact {
You can also use viewWillTransitionToSize:withTransitionCoordinator: if you are looking for animation to run alongside the size change animation.
Building an Adaptive Interface from apple developer
If your Auto Layout constraints are insufficient to achieve the look
you want, you can use the
viewWillTransitionToSize:withTransitionCoordinator: method to make
changes to your layout. You can also use that method to create
additional animations to run alongside the size-change animations. For
example, during an interface rotation, you might use the transition
coordinator’s targetTransform property to to create a counter-rotation
matrix for parts of your interface.
Could it be that your actual test device is an iPhone and your simulator is an iPad? The traitCollectionDidChange method will not be called on the latter, since both orientations would be 'regular'.

pnotify prompt dialog not showing in an ember project

I'm using the pnotify error and success dialogs without any trouble, but the prompt dialog doesn't want to show. I get a Uncaught TypeError: Cannot read property 'addClass' of undefined error.
Has anybody been successful in getting this to work?
jsbin: pnotify with ember
Here is a working example: http://jsbin.com/fuqoke/1/
So what is the problem. Well, Ember uses prototype extensions. This polyfills ECMAScript 5 array methods in browsers that do not implement them, adds convenience methods and properties to built-in arrays, and makes array mutations observable. The problem is that pnotify expects to work with normal oldschool javascript arrays. For example this code snippet where it wants to loop over an array:
...
for (var i in options.buttons) {
btn = options.buttons[i];
...
This loop will also iterate over the methods added to arrays by Ember (e.g. addObjects, firstObject,....)
So 2 ways to solve this:
1) You modify the pnotify code, and contribute it ;), with a more robust implementation to loop arrays
...
for (var i = 0; i < options.buttons.length; i++) {
btn = options.buttons[i];
...
or
...
for (var i in options.buttons) {
if(options.buttons.hasOwnProperty(i)){
btn = options.buttons[i];
...
2) You disable Ember's prototype extensions, which I don't recommend. Here you can find more info:http://emberjs.com/guides/configuring-ember/disabling-prototype-extensions/ but it will get you in a lot of trouble.
To be short it is definitely a bug in pnotify.

"Cannot call method 'destroy' of undefined" in arrayWillChange

I have what I think is a pretty standard array/template relationship setup, but when I push a new item into the array I get the above mentioned Cannot call method 'destroy' of undefined error in the arrayWillChange method of the Ember source:
for (idx = start + removedCount - 1; idx >= start; idx--) {
childView = childViews[idx];
if (removingAll) { childView.removedFromDOM = true; }
childView.destroy(); <-- childView is undefined
}
I have never had this issue before. This doesn't happen when I remove an item from the array. Only on addition. Below is a link for a JSBin where I tried to duplicate the issue. The error doesn't get thrown but the template doesn't update either.
http://jsbin.com/asemul/2
EDIT:
You're calling array.push instead of array.pushObject -- the latter is an Ember.js method that is binding aware, which means it will automatically update bindings for you. The handlebars template helper {{#each filters}} is a binding to the filters array of the controller, and the template needs to know to update when the underlying array is updated. push doesn't tell the binding to update, but pushObject does.
Here's a working example (all I did was change push to pushObject): http://jsbin.com/asemul/6/
This is a pretty common mistake -- usually, I find that if my templates aren't synchronized with the underlying object, it's because something's wrong with the bindings, so that's where I start looking.
END EDIT
I don't think you should be setting removedFromDOM directly -- try using childView.remove() followed by destroy().
I'm not sure what the context is, but have you looked at ContainerView or CollectionView? Both of those views have support for arrays of child views and may accomplish what you're looking to do both more robustly and with less code.