diff --git a/Frameworks/Account/AccountError.swift b/Frameworks/Account/AccountError.swift
index 090cb268d..480871f2b 100644
--- a/Frameworks/Account/AccountError.swift
+++ b/Frameworks/Account/AccountError.swift
@@ -48,8 +48,8 @@ public enum AccountError: LocalizedError {
 		case .wrappedError(let error, _):
 			switch error {
 			case TransportError.httpError(let status):
-				if status == 401 {
-					return NSLocalizedString("Please update your credentials for this account.", comment: "Try later")
+				if status == 401  || status == 403 {
+					return NSLocalizedString("Please update your credentials for this account, or ensure that your account with this service is still valid.", comment: "Expired credentials")
 				} else {
 					return NSLocalizedString("Please try again later.", comment: "Try later")
 				}
diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj
index 25245494f..4fa40a99d 100644
--- a/NetNewsWire.xcodeproj/project.pbxproj
+++ b/NetNewsWire.xcodeproj/project.pbxproj
@@ -122,6 +122,7 @@
 		51C452AF2265108300C03939 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; };
 		51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; };
 		51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
+		51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */; };
 		51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
 		51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
 		51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; };
@@ -733,6 +734,7 @@
 		51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = "<group>"; };
 		51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
 		51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
+		51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = "<group>"; };
 		51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = "<group>"; };
 		51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
 		51E3EB3C229AB08300645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
@@ -1134,6 +1136,7 @@
 			isa = PBXGroup;
 			children = (
 				51C45264226508F600C03939 /* MasterFeedViewController.swift */,
+				51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */,
 				51C45260226508F600C03939 /* Cell */,
 			);
 			path = MasterFeed;
@@ -2469,6 +2472,7 @@
 				51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */,
 				519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */,
 				51E595AD228E1C2100FCC42B /* AddAccountViewController.swift in Sources */,
+				51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */,
 				51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */,
 				51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */,
 				51934CD023108953006127BE /* ActivityID.swift in Sources */,
diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift
index 2c4bd27c6..9a51a1e7d 100644
--- a/Shared/Activity/ActivityManager.swift
+++ b/Shared/Activity/ActivityManager.swift
@@ -20,6 +20,10 @@ class ActivityManager {
 	private var selectingActivity: NSUserActivity? = nil
 	private var readingActivity: NSUserActivity? = nil
 
+	init() {
+		NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
+	}
+	
 	func selectingToday() {
 		let title = NSLocalizedString("See articles for Today", comment: "Today")
 		selectingActivity = makeSelectingActivity(type: ActivityType.selectToday, title: title, identifier: "smartfeed.today")
@@ -41,7 +45,7 @@ class ActivityManager {
 	func selectingFolder(_ folder: Folder) {
 		let localizedText = NSLocalizedString("See articles in  “%@”", comment: "See articles in Folder")
 		let title = NSString.localizedStringWithFormat(localizedText as NSString, folder.nameForDisplay) as String
-		selectingActivity = makeSelectingActivity(type: ActivityType.selectFolder, title: title, identifier: "folder.\(folder.nameForDisplay)")
+		selectingActivity = makeSelectingActivity(type: ActivityType.selectFolder, title: title, identifier: identifer(for: folder))
 	 
 		selectingActivity!.userInfo = [
 			ActivityID.accountID.rawValue: folder.account?.accountID ?? "",
@@ -55,14 +59,15 @@ class ActivityManager {
 	func selectingFeed(_ feed: Feed) {
 		let localizedText = NSLocalizedString("See articles in  “%@”", comment: "See articles in Feed")
 		let title = NSString.localizedStringWithFormat(localizedText as NSString, feed.nameForDisplay) as String
-		selectingActivity = makeSelectingActivity(type: ActivityType.selectFeed, title: title, identifier: feed.url)
+		selectingActivity = makeSelectingActivity(type: ActivityType.selectFeed, title: title, identifier: identifer(for: feed))
 		
 		selectingActivity!.userInfo = [
 			ActivityID.accountID.rawValue: feed.account?.accountID ?? "",
 			ActivityID.accountName.rawValue: feed.account?.name ?? "",
 			ActivityID.feedID.rawValue: feed.feedID
 		]
-
+		updateSelectingActivityFeedSearchAttributes(with: feed)
+		
 		selectingActivity!.becomeCurrent()
 	}
 	
@@ -74,6 +79,46 @@ class ActivityManager {
 		readingActivity?.becomeCurrent()
 	}
 	
+	func cleanUp(_ account: Account) {
+		var ids = [String]()
+		
+		if let folders = account.folders {
+			for folder in folders {
+				ids.append(identifer(for: folder))
+			}
+		}
+		
+		for feed in account.flattenedFeeds() {
+			ids.append(contentsOf: identifers(for: feed))
+		}
+		
+		NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: ids) {}
+	}
+	
+	func cleanUp(_ folder: Folder) {
+		var ids = [String]()
+		ids.append(identifer(for: folder))
+		
+		for feed in folder.flattenedFeeds() {
+			ids.append(contentsOf: identifers(for: feed))
+		}
+		
+		NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: ids) {}
+	}
+	
+	func cleanUp(_ feed: Feed) {
+		NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: identifers(for: feed)) {}
+	}
+	
+	@objc func feedIconDidBecomeAvailable(_ note: Notification) {
+		guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[ActivityID.feedID.rawValue] as? String else {
+			return
+		}
+		if activityFeedId == feed.feedID {
+			updateSelectingActivityFeedSearchAttributes(with: feed)
+		}
+	}
+
 }
 
 // MARK: Private
@@ -110,13 +155,13 @@ private extension ActivityManager {
 		activity.isEligibleForSearch = true
 		activity.isEligibleForPrediction = false
 		activity.isEligibleForHandoff = true
+		activity.persistentIdentifier = identifer(for: article)
 		
 		// CoreSpotlight
 		let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeCompositeContent as String)
 		attributeSet.title = article.title
 		attributeSet.contentDescription = article.summary
 		attributeSet.keywords = keywords
-		attributeSet.relatedUniqueIdentifier = article.url
 		
 		if let image = article.avatarImage() {
 			attributeSet.thumbnailData = image.pngData()
@@ -131,4 +176,43 @@ private extension ActivityManager {
 		return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? []
 	}
 	
+	func identifer(for folder: Folder) -> String {
+		return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)"
+	}
+	
+	func identifer(for feed: Feed) -> String {
+		return "account_\(feed.account!.accountID)_feed_\(feed.feedID)"
+	}
+	
+	func identifer(for article: Article) -> String {
+		return "account_\(article.accountID)_feed_\(article.feedID)_article_\(article.articleID)"
+	}
+	
+	func identifers(for feed: Feed) -> [String] {
+		var ids = [String]()
+		ids.append(identifer(for: feed))
+		
+		for article in feed.fetchArticles() {
+			ids.append(identifer(for: article))
+		}
+		
+		return ids
+	}
+	
+	func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
+		
+		let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
+		attributeSet.title = feed.nameForDisplay
+		attributeSet.keywords = makeKeywords(feed.nameForDisplay)
+		if let image = appDelegate.feedIconDownloader.icon(for: feed) {
+			attributeSet.thumbnailData = image.pngData()
+		} else if let image = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) {
+			attributeSet.thumbnailData = image.pngData()
+		}
+		
+		selectingActivity!.contentAttributeSet = attributeSet
+		selectingActivity!.needsSave = true
+		
+	}
+	
 }
diff --git a/Shared/Commands/DeleteCommand.swift b/Shared/Commands/DeleteCommand.swift
index 6d1bda0e3..6c62e0554 100644
--- a/Shared/Commands/DeleteCommand.swift
+++ b/Shared/Commands/DeleteCommand.swift
@@ -54,24 +54,6 @@ final class DeleteCommand: UndoableCommand {
 		registerUndo()
 	}
 
-	func perform(completion: @escaping () -> Void) {
-		
-		let group = DispatchGroup()
-		group.enter()
-		itemSpecifiers.forEach {
-			$0.delete() {
-				group.leave()
-			}
-		}
-		treeController.rebuild()
-	
-		group.notify(queue: DispatchQueue.main) {
-			self.registerUndo()
-			completion()
-		}
-		
-	}
-	
 	func undo() {
 
 		BatchUpdate.shared.perform {
diff --git a/iOS/AppCoordinator.swift b/iOS/AppCoordinator.swift
index 7df057dad..e87596f8c 100644
--- a/iOS/AppCoordinator.swift
+++ b/iOS/AppCoordinator.swift
@@ -76,8 +76,12 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
 		return treeController.rootNode
 	}
 	
-	var numberOfSections: Int {
-		return shadowTable.count
+	var allSections: [Int] {
+		var sections = [Int]()
+		for (index, _) in shadowTable.enumerated() {
+			sections.append(index)
+		}
+		return sections
 	}
 
 	private(set) var currentMasterIndexPath: IndexPath? {
@@ -313,14 +317,6 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
 
 	// MARK: API
 	
-	func beginUpdates() {
-		animatingChanges = true
-	}
-
-	func endUpdates() {
-		animatingChanges = false
-	}
-	
 	func rowsInSection(_ section: Int) -> Int {
 		return shadowTable[section].count
 	}
@@ -361,6 +357,10 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
 		return shadowTable[indexPath.section][indexPath.row]
 	}
 	
+	func nodesFor(section: Int) -> [Node] {
+		return shadowTable[section]
+	}
+	
 	func indexPathFor(_ node: Node) -> IndexPath? {
 		for i in 0..<shadowTable.count {
 			if let row = shadowTable[i].firstIndex(of: node) {
@@ -388,8 +388,7 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
 		return 0
 	}
 		
-	func expand(section: Int, completion: ([IndexPath]) -> ()) {
-		
+	func expand(section: Int) {
 		guard let expandNode = treeController.rootNode.childAtIndex(section) else {
 			return
 		}
@@ -397,11 +396,9 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
 		
 		animatingChanges = true
 		
-		var indexPathsToInsert = [IndexPath]()
 		var i = 0
 		
 		func addNode(_ node: Node) {
-			indexPathsToInsert.append(IndexPath(row: i, section: section))
 			shadowTable[section].insert(node, at: i)
 			i = i + 1
 		}
@@ -415,36 +412,26 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
 			}
 		}
 		
-		completion(indexPathsToInsert)
-		
 		animatingChanges = false
-		
 	}
 	
-	func expand(_ indexPath: IndexPath, completion: ([IndexPath]) -> ()) {
-		
+	func expand(_ indexPath: IndexPath) {
 		let expandNode = shadowTable[indexPath.section][indexPath.row]
 		expandedNodes.append(expandNode)
 		
 		animatingChanges = true
 		
-		var indexPathsToInsert = [IndexPath]()
 		for i in 0..<expandNode.childNodes.count {
 			if let child = expandNode.childAtIndex(i) {
 				let nextIndex = indexPath.row + i + 1
-				indexPathsToInsert.append(IndexPath(row: nextIndex, section: indexPath.section))
 				shadowTable[indexPath.section].insert(child, at: nextIndex)
 			}
 		}
 		
-		completion(indexPathsToInsert)
-		
 		animatingChanges = false
-		
 	}
 	
-	func collapse(section: Int, completion: ([IndexPath]) -> ()) {
-		
+	func collapse(section: Int) {
 		animatingChanges = true
 		
 		guard let collapseNode = treeController.rootNode.childAtIndex(section) else {
@@ -455,20 +442,12 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
 			expandedNodes.remove(at: removeNode)
 		}
 		
-		var indexPathsToRemove = [IndexPath]()
-		for i in 0..<shadowTable[section].count {
-			indexPathsToRemove.append(IndexPath(row: i, section: section))
-		}
 		shadowTable[section] = [Node]()
 		
-		completion(indexPathsToRemove)
-		
 		animatingChanges = false
-		
 	}
 	
-	func collapse(_ indexPath: IndexPath, completion: ([IndexPath]) -> ()) {
-		
+	func collapse(_ indexPath: IndexPath) {
 		animatingChanges = true
 		
 		let collapseNode = shadowTable[indexPath.section][indexPath.row]
@@ -476,24 +455,13 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
 			expandedNodes.remove(at: removeNode)
 		}
 		
-		var indexPathsToRemove = [IndexPath]()
-		
-		for child in collapseNode.childNodes {
-			if let index = shadowTable[indexPath.section].firstIndex(of: child) {
-				indexPathsToRemove.append(IndexPath(row: index, section: indexPath.section))
-			}
-		}
-		
 		for child in collapseNode.childNodes {
 			if let index = shadowTable[indexPath.section].firstIndex(of: child) {
 				shadowTable[indexPath.section].remove(at: index)
 			}
 		}
 		
-		completion(indexPathsToRemove)
-		
 		animatingChanges = false
-		
 	}
 	
 	func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
diff --git a/iOS/MasterFeed/MasterFeedDataSource.swift b/iOS/MasterFeed/MasterFeedDataSource.swift
new file mode 100644
index 000000000..fe7807721
--- /dev/null
+++ b/iOS/MasterFeed/MasterFeedDataSource.swift
@@ -0,0 +1,87 @@
+//
+//  MasterFeedDataSource.swift
+//  NetNewsWire-iOS
+//
+//  Created by Maurice Parker on 8/28/19.
+//  Copyright © 2019 Ranchero Software. All rights reserved.
+//
+
+import UIKit
+import RSCore
+import RSTree
+import Account
+
+class MasterFeedDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable {
+
+	private var coordinator: AppCoordinator!
+	private var errorHandler: ((Error) -> ())!
+	
+	init(coordinator: AppCoordinator, errorHandler: @escaping (Error) -> (), tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider) {
+		super.init(tableView: tableView, cellProvider: cellProvider)
+		self.coordinator = coordinator
+		self.errorHandler = errorHandler
+	}
+	
+	override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
+		guard let node = coordinator.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else {
+			return false
+		}
+		return true
+	}
+	
+	override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
+		guard let node = coordinator.nodeFor(indexPath) else {
+			return false
+		}
+		return node.representedObject is Feed
+	}
+	
+	override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
+
+		guard let sourceNode = coordinator.nodeFor(sourceIndexPath), let feed = sourceNode.representedObject as? Feed else {
+			return
+		}
+
+		// Based on the drop we have to determine a node to start looking for a parent container.
+		let destNode: Node = {
+			if destinationIndexPath.row == 0 {
+				return coordinator.rootNode.childAtIndex(destinationIndexPath.section)!
+			} else {
+				let movementAdjustment = sourceIndexPath > destinationIndexPath ? 1 : 0
+				let adjustedDestIndexPath = IndexPath(row: destinationIndexPath.row - movementAdjustment, section: destinationIndexPath.section)
+				return coordinator.nodeFor(adjustedDestIndexPath)!
+			}
+		}()
+
+		// Now we start looking for the parent container
+		let destParentNode: Node? = {
+			if destNode.representedObject is Container {
+				return destNode
+			} else {
+				if destNode.parent?.representedObject is Container {
+					return destNode.parent!
+				} else {
+					return nil
+				}
+			}
+		}()
+		
+		// Move the Feed
+		guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else {
+			return
+		}
+		
+		BatchUpdate.shared.start()
+		source.account?.moveFeed(feed, from: source, to: destination) { result in
+			switch result {
+			case .success:
+				BatchUpdate.shared.end()
+			case .failure(let error):
+				self.errorHandler(error)
+			}
+		}
+
+	}
+	
+
+}
diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift
index 14bd25023..72bac2de0 100644
--- a/iOS/MasterFeed/MasterFeedViewController.swift
+++ b/iOS/MasterFeed/MasterFeedViewController.swift
@@ -18,9 +18,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 	@IBOutlet private weak var markAllAsReadButton: UIBarButtonItem!
 	@IBOutlet private weak var addNewItemButton: UIBarButtonItem!
 	
+	private lazy var dataSource = makeDataSource()
 	var undoableCommands = [UndoableCommand]()
-	
 	weak var coordinator: AppCoordinator!
+	
 	override var canBecomeFirstResponder: Bool {
 		return true
 	}
@@ -36,6 +37,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 		navigationItem.rightBarButtonItem = editButtonItem
 		
 		tableView.register(MasterFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
+		tableView.dataSource = dataSource
 		
 		NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
 		NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
@@ -48,6 +50,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 		refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
 		
 		updateUI()
+		applyChanges(animate: false)
 		
 	}
 
@@ -71,38 +74,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 	// MARK: Notifications
 	
 	@objc func unreadCountDidChange(_ note: Notification) {
-		
 		updateUI()
-
-		guard let representedObject = note.object else {
-			return
-		}
-		
-		if let account = representedObject as? Account {
-			if let node = coordinator.rootNode.childNodeRepresentingObject(account) {
-				let sectionIndex = coordinator.rootNode.indexOfChild(node)!
-				if let headerView = tableView.headerView(forSection: sectionIndex) as? MasterFeedTableViewSectionHeader {
-					headerView.unreadCount = account.unreadCount
-				}
-			}
-			return
-		}
-		
-		var node: Node? = nil
-		if let coordinator = representedObject as? AppCoordinator, let fetcher = coordinator.timelineFetcher {
-			node = coordinator.rootNode.descendantNodeRepresentingObject(fetcher as AnyObject)
-		} else {
-			node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject)
-		}
-		
-		guard let unwrappedNode = node, let indexPath = coordinator.indexPathFor(unwrappedNode) else {
-			return
-		}
-
-		performBlockAndRestoreSelection {
-			tableView.reloadRows(at: [indexPath], with: .none)
-		}
-
+		applyChanges(animate: false)
 	}
 
 	@objc func faviconDidBecomeAvailable(_ note: Notification) {
@@ -110,15 +83,12 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 	}
 
 	@objc func feedSettingDidChange(_ note: Notification) {
-		
 		guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else {
 			return
 		}
-		
 		if key == Feed.FeedSettingKey.homePageURL || key == Feed.FeedSettingKey.faviconURL {
 			configureCellsForRepresentedObject(feed)
 		}
-		
 	}
 	
 	@objc func userDidAddFeed(_ notification: Notification) {
@@ -133,19 +103,11 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 	}
 	
 	@objc func contentSizeCategoryDidChange(_ note: Notification) {
-		tableView.reloadData()
+		applyChanges(animate: false)
 	}
 	
 	// MARK: Table View
 	
-	override func numberOfSections(in tableView: UITableView) -> Int {
-		return coordinator.numberOfSections
-	}
-	
-	override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
-		return coordinator.rowsInSection(section)
-	}
-	
 	override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
 
 		guard let nameProvider = coordinator.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
@@ -197,26 +159,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 		return UIView(frame: CGRect.zero)
 	}
 
-	override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-		
-		let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell
-		
-		guard let node = coordinator.nodeFor(indexPath) else {
-			return cell
-		}
-		
-		configure(cell, node)
-		return cell
-
-	}
-	
-	override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
-		guard let node = coordinator.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else {
-			return false
-		}
-		return true
-	}
-	
 	override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
 		var actions = [UIContextualAction]()
 		
@@ -296,13 +238,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 		coordinator.selectFeed(indexPath)
 	}
 
-	override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
-		guard let node = coordinator.nodeFor(indexPath) else {
-			return false
-		}
-		return node.representedObject is Feed
-	}
-	
 	override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
 
 		// Adjust the index path so that it will never be in the smart feeds area
@@ -360,53 +295,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 		
 	}
 	
-	override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
-
-		guard let sourceNode = coordinator.nodeFor(sourceIndexPath), let feed = sourceNode.representedObject as? Feed else {
-			return
-		}
-
-		// Based on the drop we have to determine a node to start looking for a parent container.
-		let destNode: Node = {
-			if destinationIndexPath.row == 0 {
-				return coordinator.rootNode.childAtIndex(destinationIndexPath.section)!
-			} else {
-				let movementAdjustment = sourceIndexPath > destinationIndexPath ? 1 : 0
-				let adjustedDestIndexPath = IndexPath(row: destinationIndexPath.row - movementAdjustment, section: destinationIndexPath.section)
-				return coordinator.nodeFor(adjustedDestIndexPath)!
-			}
-		}()
-
-		// Now we start looking for the parent container
-		let destParentNode: Node? = {
-			if destNode.representedObject is Container {
-				return destNode
-			} else {
-				if destNode.parent?.representedObject is Container {
-					return destNode.parent!
-				} else {
-					return nil
-				}
-			}
-		}()
-		
-		// Move the Feed
-		guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else {
-			return
-		}
-		
-		BatchUpdate.shared.start()
-		source.account?.moveFeed(feed, from: source, to: destination) { result in
-			switch result {
-			case .success:
-				BatchUpdate.shared.end()
-			case .failure(let error):
-				self.presentError(error)
-			}
-		}
-
-	}
-	
 	// MARK: Actions
 	
 	@IBAction func settings(_ sender: UIBarButtonItem) {
@@ -449,18 +337,12 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 		
 		if coordinator.isExpanded(sectionNode) {
 			headerView.disclosureExpanded = false
-			coordinator.collapse(section: sectionIndex) { [weak self] indexPaths in
-				self?.tableView.beginUpdates()
-				self?.tableView.deleteRows(at: indexPaths, with: .automatic)
-				self?.tableView.endUpdates()
-			}
+			coordinator.collapse(section: sectionIndex)
+			self.applyChanges(animate: true)
 		} else {
 			headerView.disclosureExpanded = true
-			coordinator.expand(section: sectionIndex) { [weak self] indexPaths in
-				self?.tableView.beginUpdates()
-				self?.tableView.insertRows(at: indexPaths, with: .automatic)
-				self?.tableView.endUpdates()
-			}
+			coordinator.expand(section: sectionIndex)
+			self.applyChanges(animate: true)
 		}
 		
 	}
@@ -486,7 +368,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 
 	func reloadFeeds() {
 		updateUI()
-		tableView.reloadData()
+		applyChanges(animate: true)
 	}
 	
 	func discloseFeed(_ feed: Feed) {
@@ -506,16 +388,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
 			return
 		}
 		
-		coordinator.expand(indexPath) { [weak self] indexPaths in
-			self?.tableView.beginUpdates()
-			tableView.reloadRows(at: [indexPath], with: .automatic)
-			self?.tableView.insertRows(at: indexPaths, with: .automatic)
-			self?.tableView.endUpdates()
-		}
-		
-		if let indexPath = coordinator.indexPathFor(node) {
-			tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
-			coordinator.selectFeed(indexPath)
+		coordinator.expand(indexPath)
+		reloadNode(parent)
+
+		self.applyChanges(animate: true) { [weak self] in
+			if let indexPath = self?.coordinator.indexPathFor(node) {
+				self?.tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
+				self?.coordinator.selectFeed(indexPath)
+			}
 		}
 
 	}
@@ -544,7 +424,49 @@ private extension MasterFeedViewController {
 		markAllAsReadButton.isEnabled = coordinator.isAnyUnreadAvailable
 		addNewItemButton.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
 	}
+	
+	func reloadNode(_ node: Node) {
+		var snapshot = dataSource.snapshot()
+		snapshot.reloadItems([node])
+		dataSource.apply(snapshot)
+	}
+	
+	func applyChanges(animate: Bool, completion: (() -> Void)? = nil) {
+		
+		let selectedNode: Node? = {
+			if let selectedIndexPath = tableView.indexPathForSelectedRow {
+				return coordinator.nodeFor(selectedIndexPath)
+			} else {
+				return nil
+			}
+		}()
+		
+        var snapshot = NSDiffableDataSourceSnapshot<Int, Node>()
+		
+		let sections = coordinator.allSections
+		snapshot.appendSections(sections)
 
+		for section in sections {
+			snapshot.appendItems(coordinator.nodesFor(section: section), toSection: section)
+		}
+        
+		dataSource.apply(snapshot, animatingDifferences: animate) { [weak self] in
+			if let nodeToSelect = selectedNode, let selectingIndexPath = self?.coordinator.indexPathFor(nodeToSelect) {
+				self?.tableView.selectRow(at: selectingIndexPath, animated: false, scrollPosition: .none)
+			}
+			completion?()
+		}
+		
+	}
+
+    func makeDataSource() -> UITableViewDiffableDataSource<Int, Node> {
+		return MasterFeedDataSource(coordinator: coordinator, errorHandler: ErrorHandler.present(self), tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in
+			let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell
+			self?.configure(cell, node)
+			return cell
+		})
+    }
+	
 	func configure(_ cell: MasterFeedTableViewCell, _ node: Node) {
 		
 		cell.delegate = self
@@ -619,32 +541,18 @@ private extension MasterFeedViewController {
 		guard let indexPath = tableView.indexPath(for: cell)  else {
 			return
 		}
-		coordinator.expand(indexPath) { [weak self] indexPaths in
-			self?.tableView.beginUpdates()
-			self?.tableView.insertRows(at: indexPaths, with: .automatic)
-			self?.tableView.endUpdates()
-		}
+		coordinator.expand(indexPath)
+		self.applyChanges(animate: true)
 	}
 
 	func collapse(_ cell: MasterFeedTableViewCell) {
 		guard let indexPath = tableView.indexPath(for: cell) else {
 			return
 		}
-		coordinator.collapse(indexPath) { [weak self] indexPaths in
-			self?.tableView.beginUpdates()
-			self?.tableView.deleteRows(at: indexPaths, with: .automatic)
-			self?.tableView.endUpdates()
-		}
+		coordinator.collapse(indexPath)
+		self.applyChanges(animate: true)
 	}
 
-	func performBlockAndRestoreSelection(_ block: (() -> Void)) {
-		let indexPaths = tableView.indexPathsForSelectedRows
-		block()
-		indexPaths?.forEach { [weak self] indexPath in
-			self?.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
-		}
-	}
-	
 	func makeFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
 		return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
 			
@@ -820,7 +728,7 @@ private extension MasterFeedViewController {
 				feed.rename(to: name) { result in
 					switch result {
 					case .success:
-						break
+						self?.reloadNode(node)
 					case .failure(let error):
 						self?.presentError(error)
 					}
@@ -829,7 +737,7 @@ private extension MasterFeedViewController {
 				folder.rename(to: name) { result in
 					switch result {
 					case .success:
-						break
+						self?.reloadNode(node)
 					case .failure(let error):
 						self?.presentError(error)
 					}
@@ -851,7 +759,6 @@ private extension MasterFeedViewController {
 	}
 	
 	func delete(indexPath: IndexPath) {
-
 		guard let undoManager = undoManager,
 			let deleteNode = coordinator.nodeFor(indexPath),
 			let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], treeController: coordinator.treeController, undoManager: undoManager, errorHandler: ErrorHandler.present(self))
@@ -859,23 +766,14 @@ private extension MasterFeedViewController {
 					return
 		}
 
-		var deleteIndexPaths = [indexPath]
-		if coordinator.isExpanded(deleteNode) {
-			for i in 0..<deleteNode.numberOfChildNodes {
-				deleteIndexPaths.append(IndexPath(row: indexPath.row + 1 + i, section: indexPath.section))
-			}
+		if let folder = deleteNode.representedObject as? Folder {
+			ActivityManager.shared.cleanUp(folder)
+		} else if let feed = deleteNode.representedObject as? Feed {
+			ActivityManager.shared.cleanUp(feed)
 		}
 		
 		pushUndoableCommand(deleteCommand)
-
-		coordinator.beginUpdates()
-		deleteCommand.perform {
-			self.coordinator.treeController.rebuild()
-			self.coordinator.rebuildShadowTable()
-			self.tableView.deleteRows(at: deleteIndexPaths, with: .automatic)
-			self.coordinator.endUpdates()
-		}
-		
+		deleteCommand.perform()
 	}
 	
 }
diff --git a/iOS/Settings/SettingsDetailAccountView.swift b/iOS/Settings/SettingsDetailAccountView.swift
index 606a6f487..8bc4d5edf 100644
--- a/iOS/Settings/SettingsDetailAccountView.swift
+++ b/iOS/Settings/SettingsDetailAccountView.swift
@@ -114,6 +114,7 @@ struct SettingsDetailAccountView : View {
 		
 		func delete() {
 			AccountManager.shared.deleteAccount(account)
+			ActivityManager.shared.cleanUp(account)
 		}
 	}
 }
diff --git a/iOS/Settings/UIKit/DetailAccountViewController.swift b/iOS/Settings/UIKit/DetailAccountViewController.swift
index 43b281ada..ca01d1db9 100644
--- a/iOS/Settings/UIKit/DetailAccountViewController.swift
+++ b/iOS/Settings/UIKit/DetailAccountViewController.swift
@@ -121,6 +121,7 @@ private extension DetailAccountViewController {
 		let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in
 			guard let account = self?.account else { return }
 			AccountManager.shared.deleteAccount(account)
+			ActivityManager.shared.cleanUp(account)
 			self?.navigationController?.popViewController(animated: true)
 		}
 		alertController.addAction(markAction)