Xcode simulator not calling traitCollectionDidChange - swift3

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'.

Related

Add button in SwiftUI Stepper not hittable during XCTest

My stepper is defined as follows (Standalone WatchOS app)
Stepper(value: $myCount) {
Text("\(myCount)").font(.footnote).accessibilityIdentifier("count_label")
}.accessibilityIdentifier("my_stepper")
It is fully functional on the real / simulator devices. During a test case, defined below, I am unable to invoke the increment button. (I get an error and the button itself is not hittable, ever)
XCTAssertTrue(app.steppers["my_stepper"].waitForExistence(timeout: 10))
XCTAssertFalse(app.steppers["my_stepper"].buttons["Remove"].isEnabled)
XCTAssertTrue(app.steppers["my_stepper"].buttons["Add"].isEnabled)
-> (Error) app.steppers["my_stepper"].buttons["Add"].tap()
Error kAXErrorCannotComplete performing AXAction
kAXScrollToVisibleAction on element AX element pid
I tried to forceTap (using coordinates) with no luck. Any idea how to invoke the increment action?
While the increment and decrement buttons exist in the view stack, they are not hittable. Likely a bug in SwiftUI that impacts either WatchOS or all platforms. Best way I have found to temporarily get past the issue is using the following tutorial :
app.steppers["my_stepper"].coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)).tap()
This is a workaround that will probably fail on different devices. For me, it would only work with Ultra 49mm watchOS 9.0. Accepting this until a better answer is found.

SwiftUI: Why does ForEach need an ID?

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.

Crash during UICollectionViewCell reordering after Swift 3.0 migration

I do have a strange issue in using the reorder feature in my app's UICollectionView. We have a custom layout which is implemented to show a decoration view. The collection view uses a flow based layout. When I move the first cell from its position to last cell position for reordering of the cells, the app crashes before it calls the collection view delegate's collectionView(moveItemAt: to) method.
Attached the stack trace of this issue. You can see that crash is happening in the bridging between NSIndexPath and IndexPath. I am not sure why it is happening inside UIKit. Searching for this issue found that it appears to be bug inside UIKIt which got introduced in Swift 3.0. I tested my old build which was built before swift 3.0 migration and it works without any crashes.
Can someone tell me how can I fix this issue?
Related bugs links
UICollectionView broken after Swift 3 migration?
https://bugs.swift.org/browse/SR-2103
https://bugs.swift.org/browse/SR-2417
func handleLongGesture(_ gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.began:
if let movingPageIndexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)) {
collectionView.beginInteractiveMovementForItem(at: movingPageIndexPath)
}
case UIGestureRecognizerState.changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: collectionView))
case UIGestureRecognizerState.ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}

Chart.js Version 1 differences and can they be achieved in version 2?

I apologise if this is the wrong place but the chart.js git suggested this as the only place to get support.
I recently used version 1 of chart.js for a project and I picked this library over others because of the simpler/easier user experience. As an example the line graph, if you hover over an x axis it will highlight the closest points to your hover. In version 2 you have to hover over the actual point. It's a similar situation on the other graph types both in chart.js and other libraries like highcharts.js.
My question is simple, can we replicate the usability of version 1 in version 2 or have we lost this aspect completely?
Glancing at the documentation it doesn't appear to be possible.
If the answer is no, may I suggest that one of two things happen, either it's developed for version 2 or b version 1 is kept around.
You can replicate the v1.x functionality by extending the line chart type and setting the tooltip mode to label, like so
Chart.defaults.myLine = Chart.helpers.clone(Chart.defaults.line);
Chart.controllers.myLine = Chart.controllers.line.extend({
updateElement: function (point) {
var result = Chart.controllers.line.prototype.updateElement.apply(this, arguments);
point.inRange = function (mouseX, mouseY) {
var vm = this._view;
// ignore the y coordinate
return vm ? (Math.abs(mouseX - vm.x) < (vm.hitRadius + vm.radius)) : false;
};
return result;
}
});
and then
...
type: 'myLine',
...
options: {
tooltips: {
mode: 'label'
}
}
};
Fiddle - http://jsfiddle.net/gyqmbL2q/

Google Maps Android API v2 getVisibleRegion() returns 0

I am using the new Google Maps Android API v2 and need to detect latlng coordinates for the corners of my screen view. I use mMap.getProjection().getVisibleRegion() to do this, which works fine the first time I load the program, but if I leave the program and then re-open it (either via the back button or the home button) getVisibleRegion() returns latlng coordinates that are all (0.0, 0.0). I have a workaround where I save the VisibleRegion object as a global in the application class when I first open the program, but this seems like a bad idea. Anyone understand why the latlng coordinates go to zero (but not null) when re-opening? Thanks!
Edit: sometimes initial load gives a (0.0, 0.0) Visible Region so my workaround is not viable. Using getProjection().fromScreenLocation(point) also returns 0.
Okay, I think I solved it: I was guessing right: The map was not visible and therefore there is also no visibleRegion (in other words a region from 0,0,0,0 to 0,0,0,0). Similar to this question
Android Google Maps API v2 calling getProjection from onResume
I attached an onCameraChanged-Listener to my map. Everytime the map is moved (or the camera is available => There is a visibleRegion) this method is called.
private void initMap() {
mMap = getMap();
if (mMap != null) {
try {
mMap.setLocationSource(this);
mMap.setOnMarkerClickListener(this);
mMap.setOnCameraChangeListener(new OnCameraChangedListener() {
#Override
public void onCameraChange(CameraPosition cameraPosition) {
Log.d(TAG, "onCameraChange");
updateMapItems();
}
}
MapsInitializer.initialize(getActivity());
...
}
}
}
This listener makes sure that you have a visibleRegion.
UPDATE: Since the new update of the Google Maps API v2, there is a callback onMapReady(). I did not use it yet, but it seems to serve exactly the purpose of this question: https://developer.android.com/reference/com/google/android/gms/maps/OnMapReadyCallback.html