-
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
Refactor SharingService
to use CoreDataStack
#20265
Conversation
You can test the changes in WordPress from this Pull Request by:
|
You can test the changes in Jetpack from this Pull Request by:
|
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.
Left a few nitpicks. That's all.
static func lookupPublicizeServiceNamed(_ name: String, in context: NSManagedObjectContext) throws -> PublicizeService? { | ||
let request = NSFetchRequest<PublicizeService>(entityName: PublicizeService.classNameWithoutNamespaces()) | ||
request.predicate = NSPredicate(format: "serviceID = %@", name) | ||
return try context.fetch(request).first |
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.
Not sure I called this out before, but one great benefit of all this work is how much more Swifty the code is getting by leveraging throws
in many more places as opposed to swallowing errors or return .none
as a sign of error.
|
||
@objc(lookupPublicizeServiceNamed:inContext:) | ||
static func objc_lookupPublicizeServiceNamed(_ name: String, in context: NSManagedObjectContext) -> PublicizeService? { | ||
try? lookupPublicizeServiceNamed(name, in: context) |
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.
LOL. I just commented above about the code being better because it doesn't return .none
as a sign of error... 🙃
Of course, this is a special case method, behaving that way to fit with the Objective-C layer 😄
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.
Yeah, a little compromise for Objective-C code.
/// | ||
/// - Parameters | ||
/// - blog: A `Blog` object |
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.
Nitpick: This documentation uses a different format that the one in the file before. Which one should we use?
Regardless, how would you feel about removing it? Unlike the one above, that explains the relationship between name and serviceID
, this one doesn't explain much.
/// | |
/// - Parameters | |
/// - blog: A `Blog` object |
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.
Updated in e95fd66
|
||
|
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.
/// Finds a cached `SharingButton` by its `buttonID` for the specified `Blog` | ||
/// | ||
/// - Parameters: | ||
/// - buttonID: The button ID of the `sharingButton`. | ||
/// - blog: The blog that owns the sharing button. | ||
/// | ||
/// - Returns: The requested `SharingButton` or nil. | ||
/// | ||
static func lookupSharingButton(byID buttonID: String, for blog: Blog, in context: NSManagedObjectContext) throws -> SharingButton? { | ||
let request = NSFetchRequest<SharingButton>(entityName: SharingButton.classNameWithoutNamespaces()) | ||
request.predicate = NSPredicate(format: "buttonID = %@ AND blog = %@", buttonID, blog) | ||
return try context.fetch(request).first | ||
} |
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.
Wow... I was not expecting a button to be stored in the database 🤔
I guess there are a few properties worth storing, but still...
WordPress-iOS/WordPress/Classes/Models/SharingButton.swift
Lines 12 to 18 in f5c8424
@NSManaged open var buttonID: String | |
@NSManaged open var name: String | |
@NSManaged open var shortname: String | |
@NSManaged open var custom: Bool | |
@NSManaged open var enabled: Bool | |
@NSManaged open var visibility: String? | |
@NSManaged open var order: NSNumber |
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.
Yeah, it's pretty common in the app actually.
There are roughly three kinds of database records (a.k.a the managed objects) in the app.
- The database records are actually useful and must not be discarded. Like
Post
for local drafts. - The database records are just a cache, which is used to display UI initially but refreshed later. Like, media objects in the WordPress media library.
- The database records are saved for the convenience of displaying them using a
NSFetchedResultController
. These records can be deleted immediately after the user exists the corresponding screen, without affecting the app. Like, sharing buttons in this PR, and "managed people" in "People" screens, some posts in the Reader tab, etc.
My feeling is, the third kind is the majority.
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.
The database records are saved for the convenience of displaying them using a
NSFetchedResultController
. These records can be deleted immediately after the user exists the corresponding screen, without affecting the app.
Is the convenience of displaying them in this way worth the overhead?
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.
That's not an easy question to answer, I think 🥲 .
Here is another way to look at it. It's a super common pattern in the app to use a NSFetchedResultController
to display a list of things. It's so common that we have a WPTableViewHandler
for this exact purpose.
Now, here comes my assumption. When we start building a new feature that displays a list of stuff, I think the following is probably a part of the mindset. Naturally we would go with the existing patterns, the most common one, which is using a NSFetchedResultController
. Then we'll see what it requires. It requires a managed object? Okay, I'll add a new model to the Core Data model file then. That's probably how we end up with so many the third kind of model types I mentioned above.
And a question in my head is, when it comes to implementing screens like the "People" or the "Menus" screen (which both uses the third kind of model types I mentioned above—ManagedPerson
and Menu
are not used anywhere else in the app and the records stored in the database aren't used), do we need to create a Core Data model for it? Obviously, we surely can build them without using Core Data. We just don't have alternative to consider (maybe there is that I am not aware of).
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.
Thank you, this is super insightful. Sounds like a matter of path dependance and inertia. @alpavanoglu and I talked briefly privately about something similar in regards to adopting SwiftUI. There are already well established patterns that serve us well enough, sticking with them is often the path of least resistance.
SharingService *sharingService = [[SharingService alloc] initWithManagedObjectContext:[self managedObjectContext]]; | ||
self.publicizeServices = [sharingService allPublicizeServices]; | ||
self.publicizeServices = [PublicizeService allPublicizeServicesInContext:[self managedObjectContext] error: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.
Would it be worth to pass an error pointer here, so that we can log and bail if the method throws?
I mean... if the method throws then the data the table should show won't change and this is a no-op, so there's no much waste in not handling the error.
So I guess it all boils down to whether we want to capture and log it or not. 🤔 I don't have a strong opinion about this. I'd be tempted to say "log just in case; more information is always better" but... are those logs ever looked at? I know they are sometimes for support, but not sure how much more.
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.
Yeah, I guess capturing the error or not depending on what we are going to do with it. If it's just logging, I would probably say it's not that valuable. Especially in this case, the error (which is Core Data failing to fetch the model objects) is not likely to happen.
if value.oldValue == shared { | ||
success?() | ||
return | ||
} |
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.
Have you considered using guard
to ensure the compiler reminds us to early return?
if value.oldValue == shared { | |
success?() | |
return | |
} | |
guard value.oldValue != shared else { | |
success?() | |
return | |
} |
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 find the if statement reads more naturally: "if the old value is the same as the updated value, then we should call the success block and exist early". Whereas the double negative in the guard statement feels less nature: "make sure the old value is not the same as the updated value, otherwise we should call the success block and exist early".
WPAppAnalytics.track(.sharingPublicizeConnectionAvailableToAllChanged, withProperties: properties, withBlogID: value.siteID) | ||
|
||
self.coreDataStack.performAndSave({ context in | ||
_ = try self.createOrReplacePublicizeConnectionForBlogWithObjectID(blogObjectID, remoteConnection: remoteConnection, in: context) |
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.
Would it be useful to make the method @discaradableResult
? Or is the fact that we are calling it and ignoring it's result a warning bell?
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.
Update: There is code below that calls the method without an assignment, so I guess this was just something lost in translation here.
_ = try self.createOrReplacePublicizeConnectionForBlogWithObjectID(blogObjectID, remoteConnection: remoteConnection, in: context) | |
try self.createOrReplacePublicizeConnectionForBlogWithObjectID(blogObjectID, remoteConnection: remoteConnection, in: context) |
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.
Updated in eaea3de
if errorCode == self.SharingAPIErrorNotFound { | ||
// This is a special situation. If the call to disconnect the service returns not_found then the service | ||
// has probably already been disconnected and the call was made with stale data. | ||
// Assume this is the case and treat this error as a successful disconnect. |
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.
Wonder how often this happens 🤔
let blog = try managedObjectContext.existingObject(with: blogObjectID) as! Blog | ||
let pubConn = PublicizeConnection.createOrReplace(from: remoteConnection, in: self.managedObjectContext) | ||
pubConn.blog = blog | ||
fileprivate func createOrReplacePublicizeConnectionForBlogWithObjectID( |
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.
Could this be private
?
fileprivate func createOrReplacePublicizeConnectionForBlogWithObjectID( | |
private func createOrReplacePublicizeConnectionForBlogWithObjectID( |
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.
Updated in 27c44c7
Generated by 🚫 dangerJS |
Use
CoreDataStack
instead ofNSManagedObjectContext
inSharingService
. See the "Issue" and "Proposal" sections of #19893 for rationale behind this change.Like other similar PRs, I recommend reviewing this one commit by commit too.
Test Instructions
Only the "Sharing" screen (under the My Site tab) is affected by this change. Verifying the features in it still works should be enough, i.e. connect/disconnect to a SNS, show/hide sharing buttons.
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 are describe in the "Test Instructions" section above.
What automated tests I added (or what prevented me from doing so)
None.
PR submission checklist:
RELEASE-NOTES.txt
if necessary.