UICollectionViewCell custom subview doesn't receive focus - uicollectionviewcell

I created custom Poster view so it can be reused in multiple collection view cells (just like TVPosterView in TVUIKit). I add it directly to cell content view with all needed constraints.
The problem is when cell is focused this subview doesn't receive focus update (didUpdateFocus..) so I cannot customize it's focused / unfocused constraints etc. It's odd btw that image view inside is getting floating effect.
In case if I specify cell's preferredFocusEnvironments to return [self.posterView] + super. preferredFocusEnvironments, UI behaves as expected, but the collection view delegate method didSelect not called!
Thanks in advance for any help!

Seems didUpdateFocus not called on all subviews for the focused cell and it's system design. From docs:
After the focus is updated to a new view, the focus engine calls this
method on all focus environments that contain either the previously
focused view, the next focused view, or both, in ascending order. You
should override this method to update your app’s state in response to
changes in focus. Use the provided animation coordinator to animate
changes in visual appearance related to the update. For more
information on animation coordinators, see
UIFocusAnimationCoordinator.
Note: So it means didUpdateFocus will be called first on UICollectionViewCell, than on UIViewController subclasses, in ascending order. For subviews you need to manually register customDidUpdateFocus method that will be triggered in notification update. E.g. to update it's appearance we can use notifications (tvOS 11+), please see the example below.
func customDidUpdateFocus(isFocused: Bool, with coordinator: UIFocusAnimationCoordinator) { /* Custom logic to customize appearance */ }
// Register observer
observerToken = NotificationCenter.default.addObserver(forName: UIFocusSystem.didUpdateNotification, object: nil, queue: .main) { [weak self] (note) in
guard let self = self else { return }
guard let context = note.userInfo?[UIFocusSystem.focusUpdateContextUserInfoKey] as? UIFocusUpdateContext else { return }
guard let coordinator = note.userInfo?[UIFocusSystem.animationCoordinatorUserInfoKey] as? UIFocusAnimationCoordinator else { return }
if let prev = context.previouslyFocusedView, self.isDescendant(of: prev) {
self.didUpdateFocus(isFocused: false, with: coordinator)
} else if let next = context.nextFocusedView, self.isDescendant(of: next) {
self.didUpdateFocus(isFocused: true, with: coordinator)
}
}

Related

Updating an #ObservedObject of the MainView from the DetailView forces the exit from the DetailView [duplicate]

I'm following this SwiftUI tutorial and downloaded the project files.
I built and ran the complete project without any modifications. In the app, if I:
Toggle "Show Favorites Only" on in the list view
Tap into the "Turtle Rock" or "Chilkoot Trail" detail view
In the detail view, I toggle the favorite button (a yellow star icon)
The screen will jump back to the list view by itself.
But if I tap into the detail view of the last item ("St. Mary Lake") in the list view, I can toggle the yellow star button on and off and still stay in the same detail view.
Can anyone explain this behavior? What do I need to do to stay in the detail view without being forced to navigate back to the list view?
Well, actually it is SwiftUI defect, the View being out of view hierarchy must not be refreshed (ie. body called) - it should be updated right after next appearance. (I submitted feedback #FB7659875, and recommend to do the same for everyone affected - this is the case when duplicates are better)
Meanwhile, below is possible temporary workaround (however it will continue work even after Apple fix the issue, so it is safe). The idea is to use local view state model as intermediate between view and published property and make it updated only when view is visible.
Provided only corrected view to be replaced in mentioned project.
Tested with Xcode 11.4 / iOS 13.4 - no unexpected "jump back"
struct LandmarkList: View {
#EnvironmentObject private var userData: UserData
#State private var landmarks = [Landmark]() // local model
#State private var isVisible = false // own visibility state
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
destination: LandmarkDetail(landmark: landmark)
.environmentObject(self.userData)
) {
LandmarkRow(landmark: landmark)
}
}
}
}
.onReceive(userData.$landmarks) { array in // observe external model
if self.isVisible {
self.landmarks = array // update local only if visible
}
}
.onAppear {
self.isVisible = true // track own state
self.landmarks = self.userData.landmarks
}
.onDisappear { self.isVisible = false } // track own state
.navigationBarTitle(Text("Landmarks"))
}
}
}
this happens because in the "main" list you toggled to "show only favorites". then you change in the detail the favorites (so it is no favorite landmark anymore) and because in swiftui the source of truth was changed (favorite) this item was removed from the main list and so it cannot be shown in the detail anymore because it is no member of the main list anymore, so the detail view just navigates back and show the favorite items only.

Clarity Datagrid column input filter losing focus on first keypress after moving to next page in paginated grid

Using a clarity datagrid version 2.3
Seeing an issue where if the user starts typing into the input field of datagrid column filter, the filter input focuses out automatically as soon as a key is pressed.
Since the datagrid is paginated and server driven, this causes the API to get fired as soon as a
key is pressed after the debounce time.
The automatic focus out of the input field cause the filter to only have a single character and the API gets triggered since the debouce is only 800.
Have looked at clarity github for any reported issues, doesn't look like its reported or anyone having similar issue.
Expected behavior should be the input focus out should not happend until the user moves the cursor away or presses enter, which is when the debounce should kickin after which the api should be called.
HTML:
<clr-datagrid
(clrDgRefresh)= refreshDataGrid($event)>
...
</clr-datagrid>
TS Component:
debouncer = new Subject<any>();
ngOnInit() {
this.debouncer.asObservable().pipe(
debounceTime(800)
).subscribe(state => {
// do something here.. like call an API to filter the grid.
})
}
refreshDataGrid(state) {
this.debouncer.next(state);
}
Any help is appreciated.
Currently I'm hacking my component, to make sure the focus is not lost on the input field until done so by the user.
refreshDataGrid(state) {
const isClrFilterInputField = document.querySelector('.datagrid-filter .clr-input');
if (isClrFilterInputField instanceof HTMLElement) {
isClrFilterInputField.focus();
}
this.debouncer.next(state);
}
This is still not a clean answer, but as far as I have searched, this seems like an issue with clarity datagrid itself, until I hear from someone with a cleaner answer.
Most likely the upgrade version might have this fixed.
Yet to check that.
Unfortunately I think we designed the datagrid to emit the changes on each filter value change with debouncing intended to be done on the app side as consumers see fit.
That said, it is possible to accomplish what you describe. I've implmented a quick and dirty guard based on events but there may be better ways. I'll add code snippets here and a link to the working stackblitz at the end.
You are on the right track with the debouncer. But we don't need to debounce with time, we only need to 'debounce' on certain events.
Instead of debouncing with time, what if we debounce with an #HostListener for clicks on the filter input? (I'll leave it as an exercise for you to implement a HostListener for the focusin event since focusin bubble's up and blur does not). To do that we need:
A Hostlistener that can hear keydown.enter event on the filter input
A guard to prevent requests
A property to store the datagrid state as user enters text
In general the code needs to:
Fetch data when component inits but not after unless directed
Keep track of state events that get emitted from the datagrid
listen to keydown.enter events (and any other events like the filter input focusout - becuase it bubbles up, unlike blur)
Check that the event was generated on a datagrid filter input
dismiss the guard
make the request
re-enlist the guard
Here is a rough attempt that does that:
export class DatagridFullDemo {
refreshGuard = true; // init to true to get first run data
debouncer = new Subject<any>(); // this is now an enter key debouncer
datagridState: ClrDatagridStateInterface; // a place to store datagrid state as it is emitted
ngOnInit() {
// subscribe to the debouncer and pass the state to the doRefresh function
this.debouncer.asObservable().subscribe(state => {
this.doRefresh(state);
});
}
// a private function that takes a datagrid state
private doRefresh(state: ClrDatagridStateInterface) {
// Guard against refreshes ad only run them when true
if (this.refreshGuard) {
this.loading = true;
const filters: { [prop: string]: any[] } = {};
console.log("refresh called");
if (state.filters) {
for (const filter of state.filters) {
const { property, value } = <{ property: string; value: string }>(
filter
);
filters[property] = [value];
}
}
this.inventory
.filter(filters)
.sort(<{ by: string; reverse: boolean }>state.sort)
.fetch(state.page.from, state.page.size)
.then((result: FetchResult) => {
this.users = result.users;
this.total = result.length;
this.loading = false;
this.selectedUser = this.users[1];
// Set the guard back to false to prevent requests
this.refreshGuard = false;
});
}
}
// Listen to keydown.enter events
#HostListener("document:keydown.enter", ["$event"]) enterKeydownHandler(
event: KeyboardEvent
) {
// Use a host listener that checks the event element parent to make sure its a datagrid filter
const eventSource: HTMLElement = event.srcElement as HTMLElement;
const parentElement = eventSource.parentElement as HTMLElement;
if (parentElement.classList.contains("datagrid-filter")) {
// tell our guard its ok to refresh
this.refreshGuard = true;
// pass the latest state to the debouncer to make the request
this.debouncer.next(this.datagridState);
}
}
refresh(state: ClrDatagridStateInterface) {
this.datagridState = state;
this.debouncer.next(state);
}
}
Here is a working stackblitz: https://stackblitz.com/edit/so-60980488

How can I use .lookUp() before the stage is shown (or use it once the stage is shown)

I want to use .lookup() so that I can create an event for when the content of a TextArea is clicked, but I get null when I use textArea.lookup(".content"). After searching why this is, I found out that it returns null if called before stage.show(). My next reaction was to somehow check for an event that is cast once the stage is shown, but that event is only accessible if you have access to the stage itself, which I do not in this case. What else can I do?
Don't register the handler at the content node. Let TextArea deal with the creation of the content node on its own, register a event handler at the TextArea directly and use the pickResult of the event to determine, if the click happened inside the node with style class content.
textArea.setOnMouseClicked(evt -> {
Node n = evt.getPickResult().getIntersectedNode();
while (n != textArea) {
if (n.getStyleClass().contains("content")) {
// do something with content node
System.out.println("content: " + n);
break;
}
n = n.getParent();
}
});
Generate a layout pass on the node:
node.applyCss();
node.layout();
as defined in the answer to:
Get the height of a node in JavaFX (generate a layout pass)
After that, your lookup functions on the node should work as expected.

allowsSelectionDuringEditing property for NSTableView

Application I'm trying to develop is heavily built around being able to have editable TableViews. I'm beginning to conclude that most if not all of my problems stem from the fact that I do not see allowsSelectionDuringEditing available for NSTableView as it is for UITableView.
First, would like to get insight into why.
Second, how do I go about implementing one in my NSTableView class?
Finally, I have a NSSegmentedControl in one of my columns. So I need to implement allowSelectiongWhenFocused property somewhere, because while I have focus on the button and allowing user to use <- and -> with spacebar to select switch(es), I don't want mouse / keyboard from changing the selected row.
As an aside, while I now know how to write custom UI classes and hook them into Interface Builder, I'm struggling on when/whether to customize NSTableView, NSTableRowView, NSTableCellView or NSSegmentedControl. I've tried to understand how refuseFirstResponder works as well. Trial and error is not getting me anywhere - I fix something and break something somwehre else. If someone can suggest any other reading besides Apple documentation (sometimes I suspect if it is in English) would really appreciate it.
Here's what I would do:
Subclass NSTableRowView and override hitTest(_:). If the row is not selected then the row view returns itself instead of a control. The first click in a row selects the row and doesn't change the value of a control.
override func hitTest(_ point: NSPoint) -> NSView? {
let view = super.hitTest(point)
if view == nil || self.isSelected {
return view
}
else {
return self
}
}
Implement NSTableViewDelegate's tableView(_:rowViewForRow:) to use the custom row view:
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
return MyTableRowView()
}
Implement NSTableViewDelegate's selectionShouldChange(in:) so a row can't be deselected if the values aren't valid or other conditions aren't met.
func selectionShouldChange(in tableView: NSTableView) -> Bool {
let row = tableView.selectedRow
if (row >= 0) {
return self.validateRow(row)
}
else {
return true
}
}

Accessibility/Voice over Requirement on UIActivityIndicatorView

I am trying to provide an accessibility label for UIActivityIndicatorView (which is created programmatically in my view controllers viewDidLoad). I am setting the accessibility label as:
myIndicatorView.accessibilityLabel = #"Please wait, processing"
But when I run the application, the voice over always reads "in progress". I tried to debug on simulator using the accessibility inspector, but everytime the indicator view is in focus, it has the label as "in progress". I assume, "in progress" is default voice over text for activity indicators view, But I can not change this label. I am wondering if the activity indicator view's accessble label can never be changed.
If somebody came across this issue and found a workaround, then please help me.
It's not that you're not changing it. It's that, in the background, as the status of the progress indicator changes, iOS backend updates the label, to the appropriate status. This is overriding whatever you changed it to, because it is likely applying its own update after you change the status.
I would just leave this alone. "Please wait, processing" provides no additional information as compared to "In progress". And "In progress" is the way VoiceOver users will be accustomed to hearing an "In progress" state progress indicator announce. Changing this announcement is to a non-sighted user, what changing the image to a revolving Mickey Mouse head would be to sighted one.
If you MUST change this, what you want to do, is instead of setting the property, override the implementation of the property's getter method. To do this provide a custom implementation of UIActivityIndicatorView that does the following.
#interface MyActivityIndicator : UIActivityIndicatorView
#end
#implementation MYActivityIndicator
- (NSString*)accessibilityLabel {
if (self.isAnimating) {
return NSLocalizedString("ACTIVITY_INDICATOR_ACTIVE", nil);
} else {
return NSLocalizedString("ACTIVITY_INDICATOR_INACTIVE", nil);
}
}
UIActivityIndicatorView subclass in Swift
The implementation of the accessibilityLabel getter in UIActivityIndicatorView is dynamic based on the state of the control. Therefore, if you set its accessibilityLabel, it may change later.
The following UIActivityIndicatorView subclass overrides the default implementation of accessibilityLabel. It is based on the answer by #ChrisCM in Objective C.
class MyActivityIndicatorView: UIActivityIndicatorView {
override var accessibilityLabel: String? {
get {
if isAnimating {
return NSLocalizedString("ACTIVITY_INDICATOR_ACTIVE", comment: "");
}
else {
return NSLocalizedString("ACTIVITY_INDICATOR_INACTIVE", comment: "");
}
}
set {
super.accessibilityLabel = newValue
}
}
}
In my app, the activity indicator is visible on the screen and to VoiceOver only when it is animating. Therefore, I only need one accessibilityLabel value. The following subclass uses the default, dynamic implementation of accessibilityLabel unless set explicitly. If set, it uses that value regardless of the state.
class MyActivityIndicatorView: UIActivityIndicatorView {
private var accessibilityLabelOverride: String?
override var accessibilityLabel: String? {
get {
if accessibilityLabelOverride != nil {
return accessibilityLabelOverride
}
return super.accessibilityLabel
}
set {
accessibilityLabelOverride = newValue
}
}
}
// Example use
let activityIndicatorView = MyActivityIndicatorView(activityIndicatorStyle: .gray)
activityIndicatorView.accessibilityLabel = NSLocalizedString("ACTIVITY_INDICATOR", comment: "")

Resources