-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix text change notification observer in ChangeUsernameViewController #21003
Fix text change notification observer in ChangeUsernameViewController #21003
Conversation
DDLogInfo("User cancelled alert") | ||
}) | ||
changeUsernameAction = alertController.addDefaultActionWithTitle(Constants.Alert.change, handler: { [weak alertController] _ in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a memory leak in the original implementation: self
, the view controller, strongly references changeUsernameAction
; The action instance strongly holds onto this closure, this closure strongly captures self
.
@@ -135,44 +145,50 @@ private extension ChangeUsernameViewController { | |||
preferredStyle: .alert) | |||
alertController.addAttributeMessage(String(format: Constants.Alert.message, viewModel.selectedUsername), | |||
highlighted: viewModel.selectedUsername) | |||
alertController.addCancelActionWithTitle(Constants.Alert.cancel, handler: { [weak alertController] _ in | |||
if let textField = alertController?.textFields?.first { | |||
NotificationCenter.default.removeObserver(textField, name: UITextField.textDidChangeNotification, object: nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the intention is removing the observer that's added when creating this alert. But this line doesn't actually do that: textField
is not registered as an observer, not by this view controller at least.
textField.text == self.viewModel.selectedUsername else { | ||
DDLogInfo("Username confirmation failed") | ||
return | ||
} | ||
DDLogInfo("User changes username") | ||
NotificationCenter.default.removeObserver(textField, name: UITextField.textDidChangeNotification, object: nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same unregistering observer issue as the above comment.
NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, | ||
object: textField, | ||
queue: .main) {_ in | ||
if let text = textField.text, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This notification observer, which is never unregistered, strongly holds onto textField
.
📲 You can test the changes from this Pull Request in WordPress Alpha by scanning the QR code below to install the corresponding build.
|
📲 You can test the changes from this Pull Request in Jetpack Alpha by scanning the QR code below to install the corresponding build.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started looking at adding UI tests for this behavior, but it's quite the slog.
Approving in the meantime.
NotificationCenter.default.addObserver( | ||
self, | ||
selector: #selector(handleTextDidChangeNotification(_:)), | ||
name: UITextField.textDidChangeNotification, | ||
object: nil | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR refactors the observing notification approach to register an observer once the view controller is loaded and let iOS SDK to unregister it when the view controller is released.
+1 for using addObserver
since, as you point out, iOS handles removing it. As far as I know, the only instance in which one needs to manually remove the observers in the context of an object lifecycle is when they are using blocks
If your app targets iOS 9.0 and later or macOS 10.11 and later, you do not need to unregister an observer that you created with this function. If you forget or are unable to remove an observer, the system cleans up the next time it would have posted to it.
https://developer.apple.com/documentation/foundation/nsnotificationcenter/1415360-addobserver
You must invoke removeObserver: or removeObserver:name:object: before the system deallocates any object that addObserverForName:object:queue:usingBlock: specifies.
https://developer.apple.com/documentation/foundation/nsnotificationcenter/1411723-addobserverforname
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels somewhat fragile to me in the context of UIScene
, because (IINM) we're capturing the textDidChangeNotification
for every UITextField
in the currently running app. Is that correct?
If so, this is a good bug fix, but adopts a different kind of technical debt, so I wonder if we ought to approach it slightly differently? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. There is no need to observe all text change during the lifecyle of this view controller, even though the notification handler does nothing for notifications that it doesn't care about.
I have changed the approach to only observe the text field in the alert: 69b707a.
@@ -31,6 +32,13 @@ class ChangeUsernameViewController: SignupUsernameTableViewController { | |||
super.viewDidLoad() | |||
setupViewModel() | |||
setupUI() | |||
|
|||
NotificationCenter.default.addObserver( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think of injecting the NotificationCenter
instance? (init(... notificationCenter: NotificationCenter = .default)
.
Much of a muchness?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, the never-ending "do we DI the iOS SDK's singletons" debate 😂
FWIW I vote we do so that we can inject a NotificationCenter
under test – ensures we don't rely on shared resources (and thus can parallelize our tests)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no existing unit test for this test, nor the view model class. I had a look at the view model implementation, thinking maybe I move this text observation logic into the view model if that's a sensible thing to do in the current structure. But I couldn't quite figure out the designed interactions between the view controller, view model, and the store object.
@@ -15,7 +15,8 @@ class ChangeUsernameViewController: SignupUsernameTableViewController { | |||
} | |||
return saveItem | |||
}() | |||
private var changeUsernameAction: UIAlertAction? | |||
|
|||
private weak var confirmationController: UIAlertController? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unfortunate that UIAlertController
is such a weird bit of API – it'd be great to push down the TextViewDidChange
stuff into a UIAlertController
subclass (if such a thing were allowed, which it's not for some reason).
I really wonder if adjusting the UI (to present a full-screen page instead of a jammed-up confirmation step) would make this a lot simpler code-wise? Seems like a win on both fronts, tbh.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created #21006 as an alternative to this PR, using a static function instead of a subclass. But I think the idea is similar: encapsulating the logic within UIAlertController
.
@crazytonyli there hasn't been a choice on whether to merge this or #21006. In the interested of moving forward with the code freeze, I'd suggest merging this implementation and eventually update to get the #21006 changes if so decided—or vice versa. Feel free to choose the version you favor. cc @oguzkocer as the release manager for 22.8 |
@mokagio Sounds good! These two PRs in their current state don't have too much difference in terms of runtime behaviour. |
Relates to #20994.
When the user wants to change their username from
ChangeUsernameViewController
, the app shows a confirmation alert which has a text field to ask the user to type the new username. The confirmation button in the alert will be disabled first and turn into enabled only when the user types the new username correctly.Current implementation adds notification observer at the time of creating the alert and unregisters the observer when alert is dismissed. There are a few issues with current implementation, I'll post an inline comments to highlight them. This PR refactors the observing notification approach to register an observer once the view controller is loaded and let iOS SDK to unregister it when the view controller is released.
Test Instructions
editableUsername
, so that we can enter the "change username" screen.The "Change username" alert button should be disabled. The button should become enabled when the text in the text field matches the new username, and should become disable if it doesn't.
Regression Notes
Potential unintended areas of impact
None.
What I did to test those areas of impact (or what existing automated tests I relied on)
What's described in the test instruction section.
What automated tests I added (or what prevented me from doing so)
None.
PR submission checklist:
RELEASE-NOTES.txt
if necessary.UI Changes testing checklist: N/A