Merge branch 'master' into extension-point

This commit is contained in:
Maurice Parker 2020-04-13 10:22:29 -05:00
commit d1765b3d0c
11 changed files with 207 additions and 120 deletions

View File

@ -93,7 +93,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
return
}
refreshAll(for: account, downloadFeeds: true, completion: completion)
standardRefreshAll(for: account, completion: completion)
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
@ -203,25 +203,10 @@ final class CloudKitAccountDelegate: AccountDelegate {
let normalizedItems = OPMLNormalizer.normalize(opmlItems)
// Combine all existing web feed URLs with all the new ones
var webFeedURLs = account.flattenedWebFeedURLs
for opmlItem in normalizedItems {
if let webFeedURL = opmlItem.feedSpecifier?.feedURL {
webFeedURLs.insert(webFeedURL)
} else {
if let childItems = opmlItem.children {
for childItem in childItems {
if let webFeedURL = childItem.feedSpecifier?.feedURL {
webFeedURLs.insert(webFeedURL)
}
}
}
}
}
// TODO: remove duplicates created by import
self.accountZone.importOPML(rootExternalID: rootExternalID, items: normalizedItems) { _ in
self.refreshAll(for: account, downloadFeeds: false, completion: completion)
self.initialRefreshAll(for: account, completion: completion)
}
}
@ -499,7 +484,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
switch result {
case .success(let externalID):
account.externalID = externalID
self.refreshAll(for: account, downloadFeeds: false) { _ in }
self.initialRefreshAll(for: account) { _ in }
case .failure(let error):
os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription)
}
@ -539,9 +524,49 @@ final class CloudKitAccountDelegate: AccountDelegate {
private extension CloudKitAccountDelegate {
func refreshAll(for account: Account, downloadFeeds: Bool, completion: @escaping (Result<Void, Error>) -> Void) {
func initialRefreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
let intialWebFeedsCount = downloadFeeds ? account.flattenedWebFeeds().count : 0
func fail(_ error: Error) {
self.processAccountError(account, error)
self.refreshProgress.clear()
completion(.failure(error))
}
refreshProgress.addToNumberOfTasksAndRemaining(3)
refreshArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshProgress.completeTask()
self.accountZone.fetchChangesInZone() { result in
switch result {
case .success:
self.refreshProgress.completeTask()
self.sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshProgress.completeTask()
completion(.success(()))
case .failure(let error):
fail(error)
}
}
case .failure(let error):
fail(error)
}
}
case .failure(let error):
fail(error)
}
}
}
func standardRefreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
let intialWebFeedsCount = account.flattenedWebFeeds().count
refreshProgress.addToNumberOfTasksAndRemaining(3 + intialWebFeedsCount)
func fail(_ error: Error) {
@ -550,16 +575,12 @@ private extension CloudKitAccountDelegate {
completion(.failure(error))
}
BatchUpdate.shared.start()
accountZone.fetchChangesInZone() { result in
BatchUpdate.shared.end()
switch result {
case .success:
let webFeeds = account.flattenedWebFeeds()
if downloadFeeds {
self.refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count - intialWebFeedsCount)
}
self.refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count - intialWebFeedsCount)
self.refreshProgress.completeTask()
self.sendArticleStatus(for: account) { result in
@ -573,11 +594,6 @@ private extension CloudKitAccountDelegate {
self.refreshProgress.completeTask()
guard downloadFeeds else {
completion(.success(()))
return
}
self.refresher.refreshFeeds(webFeeds) {
account.metadata.lastArticleFetchEndTime = Date()

View File

@ -247,24 +247,10 @@ final class CloudKitAccountZone: CloudKitZone {
}
}
default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!)))
}
}
}
query(ckQuery) { result in
switch result {
case .success(let records):
if records.count > 0 {
completion(.success(records[0].externalID))
} else {
self.createContainer(name: "Account", isAccount: true, completion: completion)
}
case .failure:
self.createContainer(name: "Account", isAccount: true, completion: completion)
}
}
}
func createFolder(name: String, completion: @escaping (Result<String, Error>) -> Void) {

View File

@ -26,54 +26,77 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
self.refreshProgress = refreshProgress
}
func cloudKitDidChange(record: CKRecord) {
switch record.recordType {
case CloudKitAccountZone.CloudKitWebFeed.recordType:
addOrUpdateWebFeed(record)
case CloudKitAccountZone.CloudKitContainer.recordType:
addOrUpdateContainer(record)
default:
assertionFailure("Unknown record type: \(record.recordType)")
}
}
func cloudKitDidDelete(recordKey: CloudKitRecordKey) {
switch recordKey.recordType {
case CloudKitAccountZone.CloudKitWebFeed.recordType:
removeWebFeed(recordKey.recordID.externalID)
case CloudKitAccountZone.CloudKitContainer.recordType:
removeContainer(recordKey.recordID.externalID)
default:
assertionFailure("Unknown record type: \(recordKey.recordType)")
}
}
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
completion(.success(()))
for deletedRecordKey in deleted {
switch deletedRecordKey.recordType {
case CloudKitAccountZone.CloudKitWebFeed.recordType:
removeWebFeed(deletedRecordKey.recordID.externalID)
case CloudKitAccountZone.CloudKitContainer.recordType:
removeContainer(deletedRecordKey.recordID.externalID)
default:
assertionFailure("Unknown record type: \(deletedRecordKey.recordType)")
}
}
let group = DispatchGroup()
for changedRecord in changed {
switch changedRecord.recordType {
case CloudKitAccountZone.CloudKitWebFeed.recordType:
group.enter()
addOrUpdateWebFeed(changedRecord) {
group.leave()
}
case CloudKitAccountZone.CloudKitContainer.recordType:
group.enter()
addOrUpdateContainer(changedRecord) {
group.leave()
}
default:
assertionFailure("Unknown record type: \(changedRecord.recordType)")
}
}
group.notify(queue: DispatchQueue.main) {
completion(.success(()))
}
}
func addOrUpdateWebFeed(_ record: CKRecord) {
func addOrUpdateWebFeed(_ record: CKRecord, completion: @escaping () -> Void) {
guard let account = account,
let urlString = record[CloudKitAccountZone.CloudKitWebFeed.Fields.url] as? String,
let containerExternalIDs = record[CloudKitAccountZone.CloudKitWebFeed.Fields.containerExternalIDs] as? [String],
let url = URL(string: urlString) else { return }
let url = URL(string: urlString) else {
completion()
return
}
let editedName = record[CloudKitAccountZone.CloudKitWebFeed.Fields.editedName] as? String
if let webFeed = account.existingWebFeed(withExternalID: record.externalID) {
updateWebFeed(webFeed, editedName: editedName, containerExternalIDs: containerExternalIDs)
completion()
} else {
var webFeed: WebFeed? = nil
let group = DispatchGroup()
for containerExternalID in containerExternalIDs {
group.enter()
if let container = account.existingContainer(withExternalID: containerExternalID) {
if webFeed == nil {
webFeed = createWebFeed(url: url, editedName: editedName, webFeedExternalID: record.externalID)
createWebFeedIfNecessary(url: url, editedName: editedName, webFeedExternalID: record.externalID, container: container) { webFeed in
group.leave()
}
container.addWebFeed(webFeed!)
} else {
addUnclaimedWebFeed(url: url, editedName: editedName, webFeedExternalID: record.externalID, containerExternalID: containerExternalID)
group.leave()
}
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
}
@ -83,11 +106,14 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
}
}
func addOrUpdateContainer(_ record: CKRecord) {
func addOrUpdateContainer(_ record: CKRecord, completion: @escaping () -> Void) {
guard let account = account,
let name = record[CloudKitAccountZone.CloudKitContainer.Fields.name] as? String,
let isAccount = record[CloudKitAccountZone.CloudKitContainer.Fields.isAccount] as? String,
isAccount != "1" else { return }
isAccount != "1" else {
completion()
return
}
var folder = account.existingFolder(withExternalID: record.externalID)
folder?.name = name
@ -98,16 +124,25 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
}
if let folder = folder, let containerExternalID = folder.externalID, let unclaimedWebFeeds = unclaimedWebFeeds[containerExternalID] {
let group = DispatchGroup()
for unclaimedWebFeed in unclaimedWebFeeds {
var webFeed = account.existingWebFeed(withExternalID: unclaimedWebFeed.webFeedExternalID)
if webFeed == nil {
webFeed = createWebFeed(url: unclaimedWebFeed.url, editedName: unclaimedWebFeed.editedName, webFeedExternalID: unclaimedWebFeed.webFeedExternalID)
}
if let webFeed = webFeed {
folder.addWebFeed(webFeed)
group.enter()
createWebFeedIfNecessary(url: unclaimedWebFeed.url, editedName: unclaimedWebFeed.editedName, webFeedExternalID: unclaimedWebFeed.webFeedExternalID, container: folder) { webFeed in
group.leave()
}
}
self.unclaimedWebFeeds.removeValue(forKey: containerExternalID)
group.notify(queue: DispatchQueue.main) {
self.unclaimedWebFeeds.removeValue(forKey: containerExternalID)
completion()
}
} else {
completion()
}
}
@ -144,22 +179,31 @@ private extension CloudKitAcountZoneDelegate {
}
}
func createWebFeed(url: URL, editedName: String?, webFeedExternalID: String) -> WebFeed? {
guard let account = account else { return nil }
func createWebFeedIfNecessary(url: URL, editedName: String?, webFeedExternalID: String, container: Container, completion: @escaping (WebFeed) -> Void) {
guard let account = account else { return }
if let webFeed = account.existingWebFeed(withExternalID: webFeedExternalID) {
completion(webFeed)
return
}
let webFeed = account.createWebFeed(with: editedName, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
webFeed.editedName = editedName
webFeed.externalID = webFeedExternalID
container.addWebFeed(webFeed)
refreshProgress?.addToNumberOfTasksAndRemaining(1)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress?.completeTask()
if let parsedFeed = parsedFeed {
account.update(webFeed, with: parsedFeed, {_ in })
account.update(webFeed, with: parsedFeed, { _ in
completion(webFeed)
})
} else {
completion(webFeed)
}
}
return webFeed
}
func addUnclaimedWebFeed(url: URL, editedName: String?, webFeedExternalID: String, containerExternalID: String) {

View File

@ -37,14 +37,6 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
self.refreshProgress = refreshProgress
}
func cloudKitDidChange(record: CKRecord) {
}
func cloudKitDidDelete(recordKey: CloudKitRecordKey) {
// Article downloads clean up old articles and statuses
}
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
database.selectPendingReadStatusArticleIDs() { result in
@ -118,8 +110,13 @@ private extension CloudKitArticlesZoneDelegate {
webFeeds.forEach { $0.dropConditionalGetInfo() }
self.refreshProgress?.addToNumberOfTasksAndRemaining(webFeeds.count)
self.refresher.refreshFeeds(webFeeds) {
if webFeeds.isEmpty {
group.leave()
} else {
self.refresher.refreshFeeds(webFeeds) {
group.leave()
}
}
}

View File

@ -25,8 +25,6 @@ enum CloudKitZoneError: LocalizedError {
}
protocol CloudKitZoneDelegate: class {
func cloudKitDidChange(record: CKRecord);
func cloudKitDidDelete(recordKey: CloudKitRecordKey)
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void);
}
@ -492,24 +490,13 @@ extension CloudKitZone {
}
}
op.recordChangedBlock = { [weak self] record in
guard let self = self else { return }
op.recordChangedBlock = { record in
changedRecords.append(record)
DispatchQueue.main.async {
self.delegate?.cloudKitDidChange(record: record)
}
}
op.recordWithIDWasDeletedBlock = { [weak self] recordID, recordType in
guard let self = self else { return }
op.recordWithIDWasDeletedBlock = { recordID, recordType in
let recordKey = CloudKitRecordKey(recordType: recordType, recordID: recordID)
deletedRecordKeys.append(recordKey)
DispatchQueue.main.async {
self.delegate?.cloudKitDidDelete(recordKey: recordKey)
}
}
op.recordZoneFetchCompletionBlock = { [weak self] zoneID ,token, _, _, error in

View File

@ -25,7 +25,7 @@ final class OPMLNormalizer {
items.forEach { (item) in
if let _ = item.feedSpecifier {
if !feedsToAdd.contains(where: { $0.feedSpecifier?.feedURL == item.feedSpecifier?.feedURL } ) {
if !feedsToAdd.contains(where: { $0.feedSpecifier?.feedURL == item.feedSpecifier?.feedURL }) {
feedsToAdd.append(item)
}
return
@ -39,15 +39,21 @@ final class OPMLNormalizer {
return
}
normalizedOPMLItems.append(item)
feedsToAdd.append(item)
if let itemChildren = item.children {
normalize(itemChildren, parentFolder: item)
if let parentFolder = parentFolder {
normalize(itemChildren, parentFolder: parentFolder)
} else {
normalize(itemChildren, parentFolder: item)
}
}
}
if let parentFolder = parentFolder {
for feed in feedsToAdd {
parentFolder.addChild(feed)
if !(parentFolder.children?.contains(where: { $0.feedSpecifier?.feedURL == feed.feedSpecifier?.feedURL}) ?? false) {
parentFolder.addChild(feed)
}
}
} else {
for feed in feedsToAdd {

View File

@ -167,6 +167,7 @@
517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
517630052336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
517630232336657E00E15FFF /* WebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* WebViewProvider.swift */; };
517A745B2443665000B553B9 /* UIPageViewController-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517A745A2443665000B553B9 /* UIPageViewController-Extensions.swift */; };
5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */; };
5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */; };
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; };
@ -1418,6 +1419,7 @@
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = "<group>"; };
517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = "<group>"; };
517630222336657E00E15FFF /* WebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProvider.swift; sourceTree = "<group>"; };
517A745A2443665000B553B9 /* UIPageViewController-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIPageViewController-Extensions.swift"; sourceTree = "<group>"; };
5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = "<group>"; };
5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = "<group>"; };
5183CCE4226F4DFA0010922C /* RefreshInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshInterval.swift; sourceTree = "<group>"; };
@ -2083,6 +2085,7 @@
51C45245226506C800C03939 /* UIKit Extensions */ = {
isa = PBXGroup;
children = (
51F9F3FA23DFB25700A314FD /* Animations.swift */,
51F85BFA2275D85000C787DC /* Array-Extensions.swift */,
51F85BF42273625800C787DC /* Bundle-Extensions.swift */,
51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */,
@ -2100,13 +2103,13 @@
C5A6ED6C23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift */,
51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */,
51F85BF622749FA100C787DC /* UIFont-Extensions.swift */,
517A745A2443665000B553B9 /* UIPageViewController-Extensions.swift */,
51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */,
51F9F3F823DFB16300A314FD /* UITableView-Extensions.swift */,
518ED21C23D0F26000E0A862 /* UIViewController-Extensions.swift */,
51FFF0C3235EE8E5002762AA /* VibrantButton.swift */,
5186A634235EF3A800C97195 /* VibrantLabel.swift */,
5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */,
51F9F3FA23DFB25700A314FD /* Animations.swift */,
);
path = "UIKit Extensions";
sourceTree = "<group>";
@ -4152,6 +4155,7 @@
51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */,
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */,
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
517A745B2443665000B553B9 /* UIPageViewController-Extensions.swift in Sources */,
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */,
51F85BF52273625800C787DC /* Bundle-Extensions.swift in Sources */,
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */,

View File

@ -72,7 +72,7 @@ PROVISIONING_PROFILE_SPECIFIER =
```
Set `DEVELOPMENT_TEAM` to your Apple supplied development team. You can use Keychain
Acceass to [find your development team ID](/Technotes/FindingYourDevelopmentTeamID.md).
Access to [find your development team ID](/Technotes/FindingYourDevelopmentTeamID.md).
Set `ORGANIZATION_IDENTIFIER` to a reversed domain name that you control or have made up.
Note that `PROVISIONING_PROFILE_SPECIFIER` should not have a value associated with it.

View File

@ -92,8 +92,15 @@ class ArticleViewController: UIViewController {
pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
pageViewController.delegate = self
pageViewController.dataSource = self
// This code is to disallow paging if we scroll from the left edge. If this code is removed
// PoppableGestureRecognizerDelegate will allow us to both navigate back and page back at the
// same time. That is really weird when it happens.
let panGestureRecognizer = UIPanGestureRecognizer()
panGestureRecognizer.delegate = self
pageViewController.scrollViewInsidePageControl?.addGestureRecognizer(panGestureRecognizer)
pageViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pageViewController.view)
addChild(pageViewController!)
NSLayoutConstraint.activate([
@ -329,6 +336,24 @@ extension ArticleViewController: UIPageViewControllerDelegate {
}
// MARK: UIGestureRecognizerDelegate
extension ArticleViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
let point = gestureRecognizer.location(in: nil)
if point.x > 40 {
return true
}
return false
}
}
// MARK: Private
private extension ArticleViewController {

View File

@ -0,0 +1,22 @@
//
// UIPageViewController-Extensions.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 4/12/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import UIKit
extension UIPageViewController {
var scrollViewInsidePageControl: UIScrollView? {
for view in view.subviews {
if let scrollView = view as? UIScrollView {
return scrollView
}
}
return nil
}
}

@ -1 +1 @@
Subproject commit c524ce9145dfe093500325b1c758ea83f82cc090
Subproject commit 05388e4f7073b014f786cfce18782c3d61f8e378