Merge branch 'master' into accent-color-experimental
This commit is contained in:
commit
a168c2ce80
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CloudKit
|
import CloudKit
|
||||||
|
import SystemConfiguration
|
||||||
import os.log
|
import os.log
|
||||||
import SyncDatabase
|
import SyncDatabase
|
||||||
import RSCore
|
import RSCore
|
||||||
@ -84,7 +85,14 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshAll(for: account, downloadFeeds: true, completion: completion)
|
let reachability = SCNetworkReachabilityCreateWithName(nil, "apple.com")
|
||||||
|
var flags = SCNetworkReachabilityFlags()
|
||||||
|
guard SCNetworkReachabilityGetFlags(reachability!, &flags), flags.contains(.reachable) else {
|
||||||
|
completion(.success(()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
standardRefreshAll(for: account, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||||
@ -194,25 +202,10 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
let normalizedItems = OPMLNormalizer.normalize(opmlItems)
|
let normalizedItems = OPMLNormalizer.normalize(opmlItems)
|
||||||
|
|
||||||
// Combine all existing web feed URLs with all the new ones
|
// TODO: remove duplicates created by import
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.accountZone.importOPML(rootExternalID: rootExternalID, items: normalizedItems) { _ in
|
self.accountZone.importOPML(rootExternalID: rootExternalID, items: normalizedItems) { _ in
|
||||||
self.refreshAll(for: account, downloadFeeds: false, completion: completion)
|
self.initialRefreshAll(for: account, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -490,7 +483,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
switch result {
|
switch result {
|
||||||
case .success(let externalID):
|
case .success(let externalID):
|
||||||
account.externalID = externalID
|
account.externalID = externalID
|
||||||
self.refreshAll(for: account, downloadFeeds: false) { _ in }
|
self.initialRefreshAll(for: account) { _ in }
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription)
|
os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription)
|
||||||
}
|
}
|
||||||
@ -530,9 +523,49 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
private extension CloudKitAccountDelegate {
|
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)
|
refreshProgress.addToNumberOfTasksAndRemaining(3 + intialWebFeedsCount)
|
||||||
|
|
||||||
func fail(_ error: Error) {
|
func fail(_ error: Error) {
|
||||||
@ -541,16 +574,12 @@ private extension CloudKitAccountDelegate {
|
|||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
BatchUpdate.shared.start()
|
|
||||||
accountZone.fetchChangesInZone() { result in
|
accountZone.fetchChangesInZone() { result in
|
||||||
BatchUpdate.shared.end()
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
|
|
||||||
let webFeeds = account.flattenedWebFeeds()
|
let webFeeds = account.flattenedWebFeeds()
|
||||||
if downloadFeeds {
|
|
||||||
self.refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count - intialWebFeedsCount)
|
self.refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count - intialWebFeedsCount)
|
||||||
}
|
|
||||||
|
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
self.sendArticleStatus(for: account) { result in
|
self.sendArticleStatus(for: account) { result in
|
||||||
@ -564,11 +593,6 @@ private extension CloudKitAccountDelegate {
|
|||||||
|
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
guard downloadFeeds else {
|
|
||||||
completion(.success(()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.refresher.refreshFeeds(webFeeds) {
|
self.refresher.refreshFeeds(webFeeds) {
|
||||||
|
|
||||||
account.metadata.lastArticleFetchEndTime = Date()
|
account.metadata.lastArticleFetchEndTime = Date()
|
||||||
|
@ -247,24 +247,10 @@ final class CloudKitAccountZone: CloudKitZone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
DispatchQueue.main.async {
|
self.createContainer(name: "Account", isAccount: true, completion: completion)
|
||||||
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) {
|
func createFolder(name: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||||
|
@ -26,54 +26,77 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
|||||||
self.refreshProgress = refreshProgress
|
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) {
|
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
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(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func addOrUpdateWebFeed(_ record: CKRecord) {
|
func addOrUpdateWebFeed(_ record: CKRecord, completion: @escaping () -> Void) {
|
||||||
guard let account = account,
|
guard let account = account,
|
||||||
let urlString = record[CloudKitAccountZone.CloudKitWebFeed.Fields.url] as? String,
|
let urlString = record[CloudKitAccountZone.CloudKitWebFeed.Fields.url] as? String,
|
||||||
let containerExternalIDs = record[CloudKitAccountZone.CloudKitWebFeed.Fields.containerExternalIDs] 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
|
let editedName = record[CloudKitAccountZone.CloudKitWebFeed.Fields.editedName] as? String
|
||||||
|
|
||||||
if let webFeed = account.existingWebFeed(withExternalID: record.externalID) {
|
if let webFeed = account.existingWebFeed(withExternalID: record.externalID) {
|
||||||
|
|
||||||
updateWebFeed(webFeed, editedName: editedName, containerExternalIDs: containerExternalIDs)
|
updateWebFeed(webFeed, editedName: editedName, containerExternalIDs: containerExternalIDs)
|
||||||
|
completion()
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
var webFeed: WebFeed? = nil
|
|
||||||
|
let group = DispatchGroup()
|
||||||
for containerExternalID in containerExternalIDs {
|
for containerExternalID in containerExternalIDs {
|
||||||
|
group.enter()
|
||||||
if let container = account.existingContainer(withExternalID: containerExternalID) {
|
if let container = account.existingContainer(withExternalID: containerExternalID) {
|
||||||
if webFeed == nil {
|
createWebFeedIfNecessary(url: url, editedName: editedName, webFeedExternalID: record.externalID, container: container) { webFeed in
|
||||||
webFeed = createWebFeed(url: url, editedName: editedName, webFeedExternalID: record.externalID)
|
group.leave()
|
||||||
}
|
}
|
||||||
container.addWebFeed(webFeed!)
|
|
||||||
} else {
|
} else {
|
||||||
addUnclaimedWebFeed(url: url, editedName: editedName, webFeedExternalID: record.externalID, containerExternalID: containerExternalID)
|
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,
|
guard let account = account,
|
||||||
let name = record[CloudKitAccountZone.CloudKitContainer.Fields.name] as? String,
|
let name = record[CloudKitAccountZone.CloudKitContainer.Fields.name] as? String,
|
||||||
let isAccount = record[CloudKitAccountZone.CloudKitContainer.Fields.isAccount] 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)
|
var folder = account.existingFolder(withExternalID: record.externalID)
|
||||||
folder?.name = name
|
folder?.name = name
|
||||||
@ -98,16 +124,25 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let folder = folder, let containerExternalID = folder.externalID, let unclaimedWebFeeds = unclaimedWebFeeds[containerExternalID] {
|
if let folder = folder, let containerExternalID = folder.externalID, let unclaimedWebFeeds = unclaimedWebFeeds[containerExternalID] {
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
|
||||||
for unclaimedWebFeed in unclaimedWebFeeds {
|
for unclaimedWebFeed in unclaimedWebFeeds {
|
||||||
var webFeed = account.existingWebFeed(withExternalID: unclaimedWebFeed.webFeedExternalID)
|
group.enter()
|
||||||
if webFeed == nil {
|
createWebFeedIfNecessary(url: unclaimedWebFeed.url, editedName: unclaimedWebFeed.editedName, webFeedExternalID: unclaimedWebFeed.webFeedExternalID, container: folder) { webFeed in
|
||||||
webFeed = createWebFeed(url: unclaimedWebFeed.url, editedName: unclaimedWebFeed.editedName, webFeedExternalID: unclaimedWebFeed.webFeedExternalID)
|
group.leave()
|
||||||
}
|
|
||||||
if let webFeed = webFeed {
|
|
||||||
folder.addWebFeed(webFeed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group.notify(queue: DispatchQueue.main) {
|
||||||
self.unclaimedWebFeeds.removeValue(forKey: containerExternalID)
|
self.unclaimedWebFeeds.removeValue(forKey: containerExternalID)
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
completion()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,22 +179,31 @@ private extension CloudKitAcountZoneDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(url: URL, editedName: String?, webFeedExternalID: String) -> WebFeed? {
|
func createWebFeedIfNecessary(url: URL, editedName: String?, webFeedExternalID: String, container: Container, completion: @escaping (WebFeed) -> Void) {
|
||||||
guard let account = account else { return nil }
|
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)
|
let webFeed = account.createWebFeed(with: editedName, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
|
||||||
webFeed.editedName = editedName
|
webFeed.editedName = editedName
|
||||||
webFeed.externalID = webFeedExternalID
|
webFeed.externalID = webFeedExternalID
|
||||||
|
container.addWebFeed(webFeed)
|
||||||
|
|
||||||
refreshProgress?.addToNumberOfTasksAndRemaining(1)
|
refreshProgress?.addToNumberOfTasksAndRemaining(1)
|
||||||
InitialFeedDownloader.download(url) { parsedFeed in
|
InitialFeedDownloader.download(url) { parsedFeed in
|
||||||
self.refreshProgress?.completeTask()
|
self.refreshProgress?.completeTask()
|
||||||
if let parsedFeed = parsedFeed {
|
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) {
|
func addUnclaimedWebFeed(url: URL, editedName: String?, webFeedExternalID: String, containerExternalID: String) {
|
||||||
|
@ -37,14 +37,6 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
|
|||||||
self.refreshProgress = refreshProgress
|
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) {
|
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
database.selectPendingReadStatusArticleIDs() { result in
|
database.selectPendingReadStatusArticleIDs() { result in
|
||||||
@ -118,9 +110,14 @@ private extension CloudKitArticlesZoneDelegate {
|
|||||||
|
|
||||||
webFeeds.forEach { $0.dropConditionalGetInfo() }
|
webFeeds.forEach { $0.dropConditionalGetInfo() }
|
||||||
self.refreshProgress?.addToNumberOfTasksAndRemaining(webFeeds.count)
|
self.refreshProgress?.addToNumberOfTasksAndRemaining(webFeeds.count)
|
||||||
|
|
||||||
|
if webFeeds.isEmpty {
|
||||||
|
group.leave()
|
||||||
|
} else {
|
||||||
self.refresher.refreshFeeds(webFeeds) {
|
self.refresher.refreshFeeds(webFeeds) {
|
||||||
group.leave()
|
group.leave()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +25,6 @@ enum CloudKitZoneError: LocalizedError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protocol CloudKitZoneDelegate: class {
|
protocol CloudKitZoneDelegate: class {
|
||||||
func cloudKitDidChange(record: CKRecord);
|
|
||||||
func cloudKitDidDelete(recordKey: CloudKitRecordKey)
|
|
||||||
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void);
|
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -492,24 +490,13 @@ extension CloudKitZone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
op.recordChangedBlock = { [weak self] record in
|
op.recordChangedBlock = { record in
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
changedRecords.append(record)
|
changedRecords.append(record)
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.delegate?.cloudKitDidChange(record: record)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
op.recordWithIDWasDeletedBlock = { [weak self] recordID, recordType in
|
op.recordWithIDWasDeletedBlock = { recordID, recordType in
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
let recordKey = CloudKitRecordKey(recordType: recordType, recordID: recordID)
|
let recordKey = CloudKitRecordKey(recordType: recordType, recordID: recordID)
|
||||||
deletedRecordKeys.append(recordKey)
|
deletedRecordKeys.append(recordKey)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.delegate?.cloudKitDidDelete(recordKey: recordKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
op.recordZoneFetchCompletionBlock = { [weak self] zoneID ,token, _, _, error in
|
op.recordZoneFetchCompletionBlock = { [weak self] zoneID ,token, _, _, error in
|
||||||
|
@ -85,12 +85,16 @@ public enum FeedIdentifier: CustomStringConvertible, Hashable {
|
|||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
case .smartFeed(let id):
|
case .smartFeed(let id):
|
||||||
|
hasher.combine("smartFeed")
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
case .script(let id):
|
case .script(let id):
|
||||||
|
hasher.combine("smartFeed")
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
case .webFeed(_, let webFeedID):
|
case .webFeed(_, let webFeedID):
|
||||||
|
hasher.combine("webFeed")
|
||||||
hasher.combine(webFeedID)
|
hasher.combine(webFeedID)
|
||||||
case .folder(_, let folderName):
|
case .folder(_, let folderName):
|
||||||
|
hasher.combine("folder")
|
||||||
hasher.combine(folderName)
|
hasher.combine(folderName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ final class OPMLNormalizer {
|
|||||||
items.forEach { (item) in
|
items.forEach { (item) in
|
||||||
|
|
||||||
if let _ = item.feedSpecifier {
|
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)
|
feedsToAdd.append(item)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -39,16 +39,22 @@ final class OPMLNormalizer {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedOPMLItems.append(item)
|
feedsToAdd.append(item)
|
||||||
if let itemChildren = item.children {
|
if let itemChildren = item.children {
|
||||||
|
if let parentFolder = parentFolder {
|
||||||
|
normalize(itemChildren, parentFolder: parentFolder)
|
||||||
|
} else {
|
||||||
normalize(itemChildren, parentFolder: item)
|
normalize(itemChildren, parentFolder: item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let parentFolder = parentFolder {
|
if let parentFolder = parentFolder {
|
||||||
for feed in feedsToAdd {
|
for feed in feedsToAdd {
|
||||||
|
if !(parentFolder.children?.contains(where: { $0.feedSpecifier?.feedURL == feed.feedSpecifier?.feedURL}) ?? false) {
|
||||||
parentFolder.addChild(feed)
|
parentFolder.addChild(feed)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for feed in feedsToAdd {
|
for feed in feedsToAdd {
|
||||||
normalizedOPMLItems.append(feed)
|
normalizedOPMLItems.append(feed)
|
||||||
|
@ -122,6 +122,7 @@
|
|||||||
517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
|
517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
|
||||||
517630052336215100E15FFF /* 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 */; };
|
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 */; };
|
5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */; };
|
||||||
5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */; };
|
5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */; };
|
||||||
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; };
|
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; };
|
||||||
@ -1339,6 +1340,7 @@
|
|||||||
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
5183CCE4226F4DFA0010922C /* RefreshInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshInterval.swift; sourceTree = "<group>"; };
|
||||||
@ -1952,6 +1954,7 @@
|
|||||||
51C45245226506C800C03939 /* UIKit Extensions */ = {
|
51C45245226506C800C03939 /* UIKit Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
51F9F3FA23DFB25700A314FD /* Animations.swift */,
|
||||||
51F85BFA2275D85000C787DC /* Array-Extensions.swift */,
|
51F85BFA2275D85000C787DC /* Array-Extensions.swift */,
|
||||||
51F85BF42273625800C787DC /* Bundle-Extensions.swift */,
|
51F85BF42273625800C787DC /* Bundle-Extensions.swift */,
|
||||||
51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */,
|
51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */,
|
||||||
@ -1969,13 +1972,13 @@
|
|||||||
C5A6ED6C23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift */,
|
C5A6ED6C23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift */,
|
||||||
51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */,
|
51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */,
|
||||||
51F85BF622749FA100C787DC /* UIFont-Extensions.swift */,
|
51F85BF622749FA100C787DC /* UIFont-Extensions.swift */,
|
||||||
|
517A745A2443665000B553B9 /* UIPageViewController-Extensions.swift */,
|
||||||
51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */,
|
51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */,
|
||||||
51F9F3F823DFB16300A314FD /* UITableView-Extensions.swift */,
|
51F9F3F823DFB16300A314FD /* UITableView-Extensions.swift */,
|
||||||
518ED21C23D0F26000E0A862 /* UIViewController-Extensions.swift */,
|
518ED21C23D0F26000E0A862 /* UIViewController-Extensions.swift */,
|
||||||
51FFF0C3235EE8E5002762AA /* VibrantButton.swift */,
|
51FFF0C3235EE8E5002762AA /* VibrantButton.swift */,
|
||||||
5186A634235EF3A800C97195 /* VibrantLabel.swift */,
|
5186A634235EF3A800C97195 /* VibrantLabel.swift */,
|
||||||
5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */,
|
5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */,
|
||||||
51F9F3FA23DFB25700A314FD /* Animations.swift */,
|
|
||||||
);
|
);
|
||||||
path = "UIKit Extensions";
|
path = "UIKit Extensions";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -3989,6 +3992,7 @@
|
|||||||
51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */,
|
51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */,
|
||||||
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */,
|
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */,
|
||||||
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
|
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
|
||||||
|
517A745B2443665000B553B9 /* UIPageViewController-Extensions.swift in Sources */,
|
||||||
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */,
|
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */,
|
||||||
51F85BF52273625800C787DC /* Bundle-Extensions.swift in Sources */,
|
51F85BF52273625800C787DC /* Bundle-Extensions.swift in Sources */,
|
||||||
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */,
|
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */,
|
||||||
|
@ -72,7 +72,7 @@ PROVISIONING_PROFILE_SPECIFIER =
|
|||||||
```
|
```
|
||||||
|
|
||||||
Set `DEVELOPMENT_TEAM` to your Apple supplied development team. You can use Keychain
|
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.
|
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.
|
Note that `PROVISIONING_PROFILE_SPECIFIER` should not have a value associated with it.
|
||||||
|
|
||||||
|
@ -6,19 +6,19 @@
|
|||||||
<body>
|
<body>
|
||||||
<outline text="inessential" title="inessential" type="rss" version="RSS" htmlUrl="https://inessential.com/" xmlUrl="https://inessential.com/feed.json"/>
|
<outline text="inessential" title="inessential" type="rss" version="RSS" htmlUrl="https://inessential.com/" xmlUrl="https://inessential.com/feed.json"/>
|
||||||
<outline text="Accidentally in Code" title="Accidentally in Code" type="rss" version="RSS" htmlUrl="https://cate.blog/" xmlUrl="https://cate.blog/feed/"/>
|
<outline text="Accidentally in Code" title="Accidentally in Code" type="rss" version="RSS" htmlUrl="https://cate.blog/" xmlUrl="https://cate.blog/feed/"/>
|
||||||
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="http://becky.coffee/" xmlUrl="https://beckyhansmeyer.com/feed/"/>
|
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="https://beckyhansmeyer.com" xmlUrl="https://beckyhansmeyer.com/feed/"/>
|
||||||
<outline text="The Shape of Everything" title="The Shape of Everything" type="rss" version="RSS" htmlUrl="https://shapeof.com/" xmlUrl="https://shapeof.com/feed.json"/>
|
<outline text="The Shape of Everything" title="The Shape of Everything" type="rss" version="RSS" htmlUrl="https://shapeof.com/" xmlUrl="https://shapeof.com/feed.json"/>
|
||||||
<outline text="Daring Fireball" title="Daring Fireball" type="rss" version="RSS" htmlUrl="https://daringfireball.net/" xmlUrl="https://daringfireball.net/feeds/json"/>
|
<outline text="Daring Fireball" title="Daring Fireball" type="rss" version="RSS" htmlUrl="https://daringfireball.net/" xmlUrl="https://daringfireball.net/feeds/json"/>
|
||||||
<outline text="Manton Reece" title="Manton Reece" type="rss" version="RSS" htmlUrl="https://manton.org/" xmlUrl="https://www.manton.org/feed/json"/>
|
<outline text="Manton Reece" title="Manton Reece" type="rss" version="RSS" htmlUrl="https://manton.org/" xmlUrl="https://www.manton.org/feed/json"/>
|
||||||
<outline text="Julia Evans" title="Julia Evans" type="rss" version="RSS" htmlUrl="https://jvns.ca/" xmlUrl="https://jvns.ca/atom.xml"/>
|
<outline text="Julia Evans" title="Julia Evans" type="rss" version="RSS" htmlUrl="https://jvns.ca/" xmlUrl="https://jvns.ca/atom.xml"/>
|
||||||
<outline text="Jason Kottke" title="Jason Kottke" type="rss" version="RSS" htmlUrl="https://kottke.org/" xmlUrl="http://feeds.kottke.org/json"/>
|
<outline text="Jason Kottke" title="Jason Kottke" type="rss" version="RSS" htmlUrl="https://kottke.org/" xmlUrl="http://feeds.kottke.org/json"/>
|
||||||
<outline text="Six Colors" title="Six Colors" type="rss" version="RSS" htmlUrl="https://sixcolors.com/" xmlUrl="https://sixcolors.com/feed.json"/>
|
<outline text="Six Colors" title="Six Colors" type="rss" version="RSS" htmlUrl="https://sixcolors.com/" xmlUrl="https://sixcolors.com/feed.json"/>
|
||||||
<outline text="Loop Insight" title="Loop Insight" type="rss" version="RSS" htmlUrl="http://www.loopinsight.com/" xmlUrl="http://www.loopinsight.com/feed/"/>
|
<outline text="Loop Insight" title="Loop Insight" type="rss" version="RSS" htmlUrl="https://www.loopinsight.com/" xmlUrl="https://www.loopinsight.com/feed/"/>
|
||||||
<outline text="NetNewsWire Blog" title="NetNewsWire Blog" type="rss" version="RSS" htmlUrl="https://nnw.ranchero.com/" xmlUrl="https://nnw.ranchero.com/feed.json"/>
|
<outline text="NetNewsWire Blog" title="NetNewsWire Blog" type="rss" version="RSS" htmlUrl="https://nnw.ranchero.com/" xmlUrl="https://nnw.ranchero.com/feed.json"/>
|
||||||
<outline text="Erica Sadun" title="Erica Sadun" type="rss" version="RSS" htmlUrl="https://ericasadun.com/" xmlUrl="https://ericasadun.com/feed/"/>
|
<outline text="Erica Sadun" title="Erica Sadun" type="rss" version="RSS" htmlUrl="https://ericasadun.com/" xmlUrl="https://ericasadun.com/feed/"/>
|
||||||
<outline text="One Foot Tsunami" title="One Foot Tsunami" type="rss" version="RSS" htmlUrl="https://onefoottsunami.com/" xmlUrl="https://onefoottsunami.com/feed/json/"/>
|
<outline text="One Foot Tsunami" title="One Foot Tsunami" type="rss" version="RSS" htmlUrl="https://onefoottsunami.com/" xmlUrl="https://onefoottsunami.com/feed/json/"/>
|
||||||
<outline text="The Omni Blog" title="The Omni Blog" type="rss" version="RSS" htmlUrl="https://www.omnigroup.com/blog/" xmlUrl="https://www.omnigroup.com/blog/rss/"/>
|
<outline text="Craig Hockenberry" title="Craig Hockenberry" type="rss" version="RSS" htmlUrl="https://furbo.org/" xmlUrl="https://furbo.org/feed/json"/>
|
||||||
<outline text="Rose Orchard" title="Rose Orchard" type="rss" version="RSS" htmlUrl="https://rosemaryorchard.com/" xmlUrl="https://rosemaryorchard.com/category/blog/feed"/>
|
<outline text="Rose Orchard" title="Rose Orchard" type="rss" version="RSS" htmlUrl="https://rosemaryorchard.com/" xmlUrl="https://rosemaryorchard.com/blog/feed/"/>
|
||||||
<outline text="Michael Tsai" title="Michael Tsai" type="rss" version="RSS" htmlUrl="https://mjtsai.com/blog/" xmlUrl="https://mjtsai.com/blog/feed/"/>
|
<outline text="Michael Tsai" title="Michael Tsai" type="rss" version="RSS" htmlUrl="https://mjtsai.com/blog/" xmlUrl="https://mjtsai.com/blog/feed/"/>
|
||||||
</body>
|
</body>
|
||||||
</opml>
|
</opml>
|
||||||
|
@ -92,8 +92,15 @@ class ArticleViewController: UIViewController {
|
|||||||
pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
|
pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
|
||||||
pageViewController.delegate = self
|
pageViewController.delegate = self
|
||||||
pageViewController.dataSource = self
|
pageViewController.dataSource = self
|
||||||
pageViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
|
// 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)
|
view.addSubview(pageViewController.view)
|
||||||
addChild(pageViewController!)
|
addChild(pageViewController!)
|
||||||
NSLayoutConstraint.activate([
|
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
|
// MARK: Private
|
||||||
|
|
||||||
private extension ArticleViewController {
|
private extension ArticleViewController {
|
||||||
|
@ -191,6 +191,7 @@ open class ImageScrollView: UIScrollView {
|
|||||||
zoomView!.addGestureRecognizer(upSwipeGesture)
|
zoomView!.addGestureRecognizer(upSwipeGesture)
|
||||||
|
|
||||||
configureImageForSize(image.size)
|
configureImageForSize(image.size)
|
||||||
|
adjustFrameToCenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureImageForSize(_ size: CGSize) {
|
private func configureImageForSize(_ size: CGSize) {
|
||||||
|
@ -36,7 +36,7 @@ class ImageViewer {
|
|||||||
var canvas = document.createElement("canvas");
|
var canvas = document.createElement("canvas");
|
||||||
canvas.width = this.img.naturalWidth;
|
canvas.width = this.img.naturalWidth;
|
||||||
canvas.height = this.img.naturalHeight;
|
canvas.height = this.img.naturalHeight;
|
||||||
canvas.getContext("2d").drawImage(this.img, 0, 0);
|
canvas.getContext("2d").drawImage(this.img, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
const rect = this.img.getBoundingClientRect();
|
const rect = this.img.getBoundingClientRect();
|
||||||
const message = {
|
const message = {
|
||||||
|
22
iOS/UIKit Extensions/UIPageViewController-Extensions.swift
Normal file
22
iOS/UIKit Extensions/UIPageViewController-Extensions.swift
Normal 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
|
Loading…
x
Reference in New Issue
Block a user