Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ target 'WordPress', :exclusive => true do
pod 'WPMediaPicker', '~> 0.7.3'
pod 'ReactiveCocoa', '~> 2.4.7'
pod 'FormatterKit', '~> 1.8.0'
pod 'RxSwift', '~> 2.1.0'
pod 'RxCocoa', '~> 2.1.0'
end

target 'WordPressTodayWidget', :exclusive => true do
Expand All @@ -53,6 +55,7 @@ target :WordPressTest, :exclusive => true do
pod 'Specta', '1.0.5'
pod 'Expecta', '0.3.2'
pod 'Nimble', '~> 3.0.0'
pod 'RxSwift', '~> 2.1.0'
end

target 'UITests', :exclusive => true do
Expand Down
7 changes: 7 additions & 0 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ PODS:
- ReactiveCocoa/no-arc (2.4.7)
- ReactiveCocoa/UI (2.4.7):
- ReactiveCocoa/Core
- RxCocoa (2.1.0):
- RxSwift (~> 2.0)
- RxSwift (2.1.0)
- Simperium (0.8.10):
- Simperium/DiffMatchPach (= 0.8.10)
- Simperium/JRSwizzle (= 0.8.10)
Expand Down Expand Up @@ -205,6 +208,8 @@ DEPENDENCIES:
- OHHTTPStubs/Swift (~> 4.6.0)
- Reachability (= 3.2)
- ReactiveCocoa (~> 2.4.7)
- RxCocoa (~> 2.1.0)
- RxSwift (~> 2.1.0)
- Simperium (= 0.8.10)
- Specta (= 1.0.5)
- SVProgressHUD (~> 1.1.3)
Expand Down Expand Up @@ -275,6 +280,8 @@ SPEC CHECKSUMS:
PDKTZipArchive: 81c4824eb5587131e422fc58b92c043c4aa96466
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
ReactiveCocoa: eb38dee0a0e698f73a9b25e5c1faea2bb4c79240
RxCocoa: 79b5feb8378545336e756a0a33fcf5e95050b71c
RxSwift: 110fb07f81c17c2c3b3254d168363057b1880d18
Simperium: f507d9b400c499048a98fe728a0b2b9956fd14c1
Specta: ac94d110b865115fe60ff2c6d7281053c6f8e8a2
SVProgressHUD: 748080e4f36e603f6c02aec292664239df5279c1
Expand Down
51 changes: 51 additions & 0 deletions WordPress/Classes/Services/AccountService+Rx.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation
import RxSwift
import RxCocoa

extension AccountService {
/// Observable that emits new values when the default account is set
///
/// - warning: This should only be observed from the main thread, otherwise behavior is undefined
var defaultAccountObjectID: Observable<NSManagedObjectID?> {
return NSNotificationCenter.defaultCenter()
.rx_notification(WPAccountDefaultWordPressComAccountChangedNotification)
.map({ ($0.object as! WPAccount?)?.objectID })
.startWith(defaultWordPressComAccount()?.objectID)
}

/// Observable that emits values when there is a change in the default account.
/// This can be that the default account is set or removed, or one of its properties changes.
///
/// - warning: This should only be observed from the main thread, otherwise behavior is undefined
var defaultAccountChanged: Observable<WPAccount?> {
// Keep a reference to the context to avoid having to reference self
// within the closure
let context = managedObjectContext

return defaultAccountObjectID
// When the default account is set return the values of this new signal
.flatMapLatest({ (objectID) -> Observable<WPAccount?> in
if let objectID = objectID,
let account = try? context.existingObjectWithID(objectID) as? WPAccount {

return NSNotificationCenter.defaultCenter()
.rx_notification(NSManagedObjectContextObjectsDidChangeNotification, object: context)
// Transform the notifications into the changed account if it changed
.map({ (note) -> WPAccount? in
guard let updatedObjects = note.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> else {
return nil
}

let matchingObject = updatedObjects.filter({ $0.objectID == objectID }).first
return matchingObject as? WPAccount
})
// A nil value here means the change didn't affect the current account
.filter({ $0 != nil })
.startWith(account)
} else {
// If the default account was removed, just send a nil value
return Observable.just(nil)
}
})
}
}
12 changes: 10 additions & 2 deletions WordPress/Classes/Services/AccountService.m
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,22 @@ - (void)setDefaultWordPressComAccount:(WPAccount *)account
[[NSUserDefaults standardUserDefaults] synchronize];

NSManagedObjectID *accountID = account.objectID;
dispatch_async(dispatch_get_main_queue(), ^{
void (^notifyAccountChange)() = ^{
NSManagedObjectContext *mainContext = [[ContextManager sharedInstance] mainContext];
NSManagedObject *accountInContext = [mainContext existingObjectWithID:accountID error:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:WPAccountDefaultWordPressComAccountChangedNotification object:accountInContext];

[[PushNotificationsManager sharedInstance] registerForRemoteNotifications];
[[InteractiveNotificationsHandler sharedInstance] registerForUserNotifications];
});
};
if ([NSThread isMainThread]) {
// This is meant to help with testing account observers.
// Short version: dispatch_async and XCTest asynchronous helpers don't play nice with each other
// Long version: see the comment in https://github.com/wordpress-mobile/WordPress-iOS/blob/2f9a2100ca69d8f455acec47a1bbd6cbc5084546/WordPress/WordPressTest/AccountServiceRxTests.swift#L7
notifyAccountChange();
} else {
dispatch_async(dispatch_get_main_queue(), notifyAccountChange);
}
}

/**
Expand Down
20 changes: 10 additions & 10 deletions WordPress/Classes/ViewRelated/MeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class MeViewController: UITableViewController, UIViewControllerRestoration {
required convenience init() {
self.init(style: .Grouped)
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "refreshModelWithNotification:", name: WPAccountDefaultWordPressComAccountChangedNotification, object: nil)
notificationCenter.addObserver(self, selector: "refreshModelWithNotification:", name: HelpshiftUnreadCountUpdatedNotification, object: nil)
}

Expand All @@ -45,12 +44,15 @@ class MeViewController: UITableViewController, UIViewControllerRestoration {
], tableView: self.tableView)

handler = ImmuTableViewHandler(takeOver: self)
reloadViewModel()
// FIXME: @koke 2015-12-17
// See https://github.com/wordpress-mobile/WordPress-iOS/issues/4416
// The view controller should observe changes to account details
// regardless of who asked for them.
// For now I'm just porting this to Swift as it is.

let context = ContextManager.sharedInstance().mainContext
let service = AccountService(managedObjectContext: context)
_ = service.defaultAccountChanged
.takeUntil(rx_deallocated)
.subscribeNext({ [unowned self] _ in
self.reloadViewModel()
})

refreshAccountDetails()

WPStyleGuide.resetReadableMarginsForTableView(tableView)
Expand Down Expand Up @@ -275,9 +277,7 @@ class MeViewController: UITableViewController, UIViewControllerRestoration {
guard let account = defaultAccount() else { return }
let context = ContextManager.sharedInstance().mainContext
let service = AccountService(managedObjectContext: context)
service.updateUserDetailsForAccount(account, success: { [weak self] in
self?.reloadViewModel()
}, failure: { _ in })
service.updateUserDetailsForAccount(account, success: { _ in }, failure: { _ in })
}

func logOut() {
Expand Down
4 changes: 4 additions & 0 deletions WordPress/WordPress.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@
E10B3652158F2D3F00419A93 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E10B3651158F2D3F00419A93 /* QuartzCore.framework */; };
E10B3654158F2D4500419A93 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E10B3653158F2D4500419A93 /* UIKit.framework */; };
E10B3655158F2D7800419A93 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 834CE7371256D0F60046A4A3 /* CoreGraphics.framework */; };
E10B5ACF1C4518E100F6A390 /* AccountService+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10B5ACE1C4518E100F6A390 /* AccountService+Rx.swift */; };
E11330511A13BAA300D36D84 /* me-sites-with-jetpack.json in Resources */ = {isa = PBXBuildFile; fileRef = E11330501A13BAA300D36D84 /* me-sites-with-jetpack.json */; };
E114D79A153D85A800984182 /* WPError.m in Sources */ = {isa = PBXBuildFile; fileRef = E114D799153D85A800984182 /* WPError.m */; };
E1209FA41BB4978B00D69778 /* PeopleService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1209FA31BB4978B00D69778 /* PeopleService.swift */; };
Expand Down Expand Up @@ -1446,6 +1447,7 @@
E10675C9183FA78E00E5CE5C /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
E10B3651158F2D3F00419A93 /* QuartzCore.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
E10B3653158F2D4500419A93 /* UIKit.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
E10B5ACE1C4518E100F6A390 /* AccountService+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AccountService+Rx.swift"; sourceTree = "<group>"; };
E11330501A13BAA300D36D84 /* me-sites-with-jetpack.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "me-sites-with-jetpack.json"; sourceTree = "<group>"; };
E114D798153D85A800984182 /* WPError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WPError.h; sourceTree = "<group>"; };
E114D799153D85A800984182 /* WPError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WPError.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2856,6 +2858,7 @@
E1209FA31BB4978B00D69778 /* PeopleService.swift */,
93C1147D18EC5DD500DAC95C /* AccountService.h */,
93C1147E18EC5DD500DAC95C /* AccountService.m */,
E10B5ACE1C4518E100F6A390 /* AccountService+Rx.swift */,
E1FD45DF1C030B3800750F4C /* AccountSettingsService.swift */,
93C1148318EDF6E100DAC95C /* BlogService.h */,
93C1148418EDF6E100DAC95C /* BlogService.m */,
Expand Down Expand Up @@ -4631,6 +4634,7 @@
5D8D53F119250412003C8859 /* BlogSelectorViewController.m in Sources */,
5D3D559718F88C3500782892 /* ReaderPostService.m in Sources */,
B532D4EE199D4418006E4DF6 /* NoteBlockImageTableViewCell.swift in Sources */,
E10B5ACF1C4518E100F6A390 /* AccountService+Rx.swift in Sources */,
93FA59DD18D88C1C001446BC /* PostCategoryService.m in Sources */,
5DCC4CD819A50CC0003E548C /* ReaderSite.m in Sources */,
93C4864F181043D700A24725 /* ActivityLogDetailViewController.m in Sources */,
Expand Down
128 changes: 128 additions & 0 deletions WordPress/WordPressTest/AccountServiceTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import UIKit
import RxSwift
import XCTest
@testable import WordPress

class AccountServiceTests: XCTestCase {
var contextManager: TestContextManager!
Expand Down Expand Up @@ -84,4 +86,130 @@ class AccountServiceTests: XCTestCase {
XCTAssertNil(accountService.defaultWordPressComAccount(), "No default account should be set")
XCTAssertTrue(account.fault, "Account should be deleted")
}

func testCreateAccountSetsDefaultAccount() {
XCTAssertNil(accountService.defaultWordPressComAccount())

let account = accountService.createOrUpdateAccountWithUsername("username", authToken: "authtoken")
XCTAssertEqual(accountService.defaultWordPressComAccount(), account)
}

func testCreateAccountDoesntReplaceDefaultAccount() {
XCTAssertNil(accountService.defaultWordPressComAccount())

let account = accountService.createOrUpdateAccountWithUsername("username", authToken: "authtoken")
XCTAssertEqual(accountService.defaultWordPressComAccount(), account)

accountService.createOrUpdateAccountWithUsername("another", authToken: "authtoken")
XCTAssertEqual(accountService.defaultWordPressComAccount(), account)
}

func testDefaultAccountObjectIDEmitsInitialValue() {
let currentObjectID = waitForValueIn(accountService.defaultAccountObjectID, block: {})

XCTAssertNil(currentObjectID)
}

func testDefaultAccountObjectIDEmitsValueWhenAccountIsSet() {
var createdAccount: WPAccount? = nil
let currentObjectID = waitForValueIn(accountService.defaultAccountObjectID, skip: 1) { [unowned self] in
createdAccount = self.accountService.createOrUpdateAccountWithUsername("username", authToken: "authtoken")
}
guard let account = createdAccount else {
XCTFail("account should not be nil")
return
}

XCTAssertEqual(currentObjectID, account.objectID)
}

func testDefaultAccountObjectIDEmitsValueWhenAccointIsRemoved() {
let currentObjectID = waitForValueIn(accountService.defaultAccountObjectID, skip: 2) { [unowned self] in
self.accountService.createOrUpdateAccountWithUsername("username", authToken: "authtoken")
self.accountService.removeDefaultWordPressComAccount()
}
XCTAssertNil(currentObjectID)
}

func testDefaultAccountChangedEmitsInitialValue() {
let value = waitForValueIn(accountService.defaultAccountChanged, block: {})

XCTAssertNil(value)
}

func testDefaultAccountChangedEmitsValueWhenPropertyIsChanged() {
let value = waitForValueIn(accountService.defaultAccountChanged, skip: 2) { [unowned self] in
// Emits initial nil (1)
// Emits account (2)
let account = self.accountService.createOrUpdateAccountWithUsername("jack", authToken: "authtoken")

// Emits account (3)
account.email = "jack@sparrow.com"
self.contextManager.saveContextAndWait(self.accountService.managedObjectContext)
}

XCTAssertNotNil(value)
XCTAssertEqual(value?.email, "jack@sparrow.com")
XCTAssertNil(value?.displayName)
}

func testDefaultAccountChangedEmitsValueWhenPropertyIsChangedAfterAnotherAccountChanges() {
let value = waitForValueIn(accountService.defaultAccountChanged, skip: 3) { [unowned self] in
// Emits initial nil (1)
// Emits account (2)
let account = self.accountService.createOrUpdateAccountWithUsername("jack", authToken: "authtoken")

// Emits account (3)
account.email = "jack@sparrow.com"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

self.contextManager.saveContextAndWait(self.accountService.managedObjectContext)

// Doesn't emit (3)
let another = self.accountService.createOrUpdateAccountWithUsername("elizabeth", authToken: "authtoken2")
another.displayName = "Elizabeth Swann"
self.contextManager.saveContextAndWait(self.accountService.managedObjectContext)

// Emits account (4)
account.displayName = "Jack Sparrow"
self.contextManager.saveContextAndWait(self.accountService.managedObjectContext)
}

XCTAssertNotNil(value)
XCTAssertEqual(value?.email, "jack@sparrow.com")
XCTAssertEqual(value?.displayName, "Jack Sparrow")
}

func testDefaultAccountChangedEmitsValueAfterAccountIsRemoved() {
let value = waitForValueIn(accountService.defaultAccountChanged, skip: 2) { [unowned self] in
// Emits initial nil (1)
// Emits account (2)
self.accountService.createOrUpdateAccountWithUsername("jack", authToken: "authtoken")

// Emits nil (3)
self.accountService.removeDefaultWordPressComAccount()
}

XCTAssertNil(value)
}

private func waitForValueIn<T>(observable: Observable<T>, skip: Int = 0, block: () -> Void) -> T {
var result: T? = nil
let expectation = expectationWithDescription("Observable completed \(observable)")
let subscription = observable.skip(skip).take(1).subscribe { (event) -> Void in
switch event {
case .Next(let value):
result = value
case .Error(let error):
XCTFail("Observable emitted error \(error)")
case .Completed:
expectation.fulfill()
}
}

block()

waitForExpectationsWithTimeout(5) { _ in
subscription.dispose()
}
return result!
}
}