Skip to content
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

Merged
merged 6 commits into from
Jul 9, 2023

Conversation

crazytonyli
Copy link
Contributor

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

  1. Modify this code in "Account Settings" to always be editableUsername, so that we can enter the "change username" screen.
  2. Launch the app from Xcode.
  3. Navigate to "Avatar" -> "Account Settings" -> "Username".
  4. Select or type a new username.
  5. Tap "Save". This is where you'll see the confirmation alert.

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

  1. Potential unintended areas of impact
    None.

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

  3. What automated tests I added (or what prevented me from doing so)
    None.

PR submission checklist:

  • I have completed the Regression Notes.
  • I have considered adding unit tests for my changes.
  • I have considered adding accessibility improvements for my changes.
  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

UI Changes testing checklist: N/A

@crazytonyli crazytonyli added this to the 22.8 milestone Jul 4, 2023
@crazytonyli crazytonyli requested review from jkmassel and mokagio July 4, 2023 04:20
@crazytonyli crazytonyli self-assigned this Jul 4, 2023
DDLogInfo("User cancelled alert")
})
changeUsernameAction = alertController.addDefaultActionWithTitle(Constants.Alert.change, handler: { [weak alertController] _ in
Copy link
Contributor Author

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)
Copy link
Contributor Author

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)
Copy link
Contributor Author

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,
Copy link
Contributor Author

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.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Jul 4, 2023

WordPress Alpha📲 You can test the changes from this Pull Request in WordPress Alpha by scanning the QR code below to install the corresponding build.
App NameWordPress Alpha WordPress Alpha
ConfigurationRelease-Alpha
Build Numberpr21003-69b707a
Version22.7
Bundle IDorg.wordpress.alpha
Commit69b707a
App Center BuildWPiOS - One-Offs #6200
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Jul 4, 2023

Jetpack Alpha📲 You can test the changes from this Pull Request in Jetpack Alpha by scanning the QR code below to install the corresponding build.
App NameJetpack Alpha Jetpack Alpha
ConfigurationRelease-Alpha
Build Numberpr21003-69b707a
Version22.7
Bundle IDcom.jetpack.alpha
Commit69b707a
App Center Buildjetpack-installable-builds #5226
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@staskus staskus self-requested a review July 4, 2023 07:38
Copy link
Contributor

@mokagio mokagio left a 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.

Comment on lines 36 to 41
NotificationCenter.default.addObserver(
self,
selector: #selector(handleTextDidChangeNotification(_:)),
name: UITextField.textDidChangeNotification,
object: nil
)
Copy link
Contributor

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

Copy link
Contributor

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? 🤔

Copy link
Contributor Author

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(
Copy link
Contributor

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?

Copy link
Contributor

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)

Copy link
Contributor Author

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?
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@mokagio
Copy link
Contributor

mokagio commented Jul 9, 2023

@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

@crazytonyli
Copy link
Contributor Author

@mokagio Sounds good! These two PRs in their current state don't have too much difference in terms of runtime behaviour.

@crazytonyli crazytonyli merged commit db15a47 into trunk Jul 9, 2023
@crazytonyli crazytonyli deleted the fix-memory-leak-in-notification-observers-7 branch July 9, 2023 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants