This commit is contained in:
Brent Simmons 2019-06-14 14:07:58 -07:00
commit c092b91349
19 changed files with 424 additions and 100 deletions

View File

@ -263,6 +263,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
switch credentials {
case .basic(let username, _):
self.username = username
default:
return
}
try CredentialsManager.storeCredentials(credentials, server: server)

View File

@ -139,7 +139,7 @@
51EF0F902279C9500050506E /* AccountsAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F8F2279C9500050506E /* AccountsAddViewController.swift */; };
51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */; };
51F35D0922AFD4760003CE1B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F35D0822AFD4760003CE1B /* SettingsView.swift */; };
51F35D1B22B001010003CE1B /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F35D1A22B001010003CE1B /* SettingsViewModel.swift */; };
51F772F622B279570087D9D1 /* SettingsDetailAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */; };
51F85BE5227217D000C787DC /* RefreshIntervalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */; };
51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BE6227245FC00C787DC /* AboutViewController.swift */; };
51F85BEB22724CB600C787DC /* About.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 51F85BEA22724CB600C787DC /* About.rtf */; };
@ -730,7 +730,7 @@
51EF0F8F2279C9500050506E /* AccountsAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddViewController.swift; sourceTree = "<group>"; };
51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddTableCellView.swift; sourceTree = "<group>"; };
51F35D0822AFD4760003CE1B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
51F35D1A22B001010003CE1B /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDetailAccountView.swift; sourceTree = "<group>"; };
51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshIntervalViewController.swift; sourceTree = "<group>"; };
51F85BE6227245FC00C787DC /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
51F85BEA22724CB600C787DC /* About.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = About.rtf; sourceTree = "<group>"; };
@ -1040,13 +1040,13 @@
5183CCEB227117C70010922C /* Settings */ = {
isa = PBXGroup;
children = (
51F35CFD22AFD0350003CE1B /* UIKit */,
51F35D0822AFD4760003CE1B /* SettingsView.swift */,
51F35D1A22B001010003CE1B /* SettingsViewModel.swift */,
510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */,
510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */,
510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */,
51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */,
510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */,
510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */,
51F35D0822AFD4760003CE1B /* SettingsView.swift */,
51F35CFD22AFD0350003CE1B /* UIKit */,
);
path = Settings;
sourceTree = "<group>";
@ -1935,12 +1935,12 @@
ORGANIZATIONNAME = "Ranchero Software";
TargetAttributes = {
6581C73220CED60000F4AD34 = {
DevelopmentTeam = M8L2WTLA8W;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Manual;
};
840D617B2029031C009BC708 = {
CreatedOnToolsVersion = 9.3;
DevelopmentTeam = M8L2WTLA8W;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.BackgroundModes = {
@ -1950,7 +1950,7 @@
};
849C645F1ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = M8L2WTLA8W;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Manual;
SystemCapabilities = {
com.apple.HardenedRuntime = {
@ -1960,7 +1960,7 @@
};
849C64701ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = 9C84TZ7Q6Z;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
TestTargetID = 849C645F1ED37A5D003D8FC0;
};
@ -2324,6 +2324,7 @@
510D707422B028E1004E8F65 /* SettingsAddAccountView.swift in Sources */,
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */,
51F772F622B279570087D9D1 /* SettingsDetailAccountView.swift in Sources */,
510D707E22B02A4B004E8F65 /* SettingsLocalAccountView.swift in Sources */,
51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */,
51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */,
@ -2348,7 +2349,6 @@
51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */,
51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */,
51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */,
51F35D1B22B001010003CE1B /* SettingsViewModel.swift in Sources */,
515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */,
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */,
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,

View File

@ -1,4 +1,6 @@
# NetNewsWire
# ![Icon](Technotes/Images/icon.png) NetNewsWire
[![CircleCI](https://circleci.com/gh/brentsimmons/NetNewsWire.svg?style=svg)](https://circleci.com/gh/brentsimmons/NetNewsWire)
Its a free and open source feed reader for macOS.

View File

@ -0,0 +1,32 @@
# NetNewsWire Branching Strategy
The main repository for NetNewsWire utilizes a [Trunk Based Development](https://trunkbaseddevelopment.com) branching strategy. This branching strategy is a variant of [Three-Flow](https://www.nomachetejuggling.com/2017/04/09/a-different-branching-strategy/).
## Three-Flow
Three-Flow uses 3 branches to facilitate development, stabilize a release, and manage production hotfixes. Development happens on Master and moves to a branch called Candidate when it is ready to be stabilized. New feature development continues on Master and bug fixes to the release candidate happen on Candidate. When the product is released, it is pushed to the Release branch. Hotfixes can happen on the Release branch. Candidate is now free to be reused to stabilize the next release. All bugs found and fixed are back merged to Candidate and then Master respectively.
![Branching](Images/Branching.png)
All arrows going up are promotions (pushes) to the next environment. All arrows going down are back ports of bugfixes.
That is Three-Flow applied to NetNewsWire. It would be that simple, but we have two products we are going to deliver from the same repository. The iOS and the macOS variants of NetNewsWire. To stabilize and manage both variants, each will need to be given their own Candidate and Release branches.
![Branching Full](Images/Branching-Full.png)
Today (6/12/2019) we have 2 branches, master and macOS Candidate, in the main repository which will eventually grow to be 5 branches.
There will also be a number of repository forks that NetNewWire developers will create to do bug fixes and implement new features (not shown here). Typically contributers will fork the Master branch to thier own repository. They would then create a feature/bugfix branch on their repository. Once work on thier forked branch is complete, they will submit a pull request to be merged back into the main repository master.
## Tagging
Each release should be tagged using [Semantic Versioning](https://semver.org/). Candidates will continue to be tagged using the current convention which denotes the difference between developer, alpha and beta releases. Additionally, we will need to use a convention to avoid tag name collisions between iOS and macOS products. macOS will use even minor release numbers and iOS will use odd minor release numbers. (See the above diagram for examples.)
## Submodules
NetNewsWire uses Git submodules to manage project dependencies. All the submodules are under the same project umbrella as NetNewWire and there are no third party dependencies to manage. These submodules are mostly stable at this point. For simplicity sake, all development on the submodules will continue on their repository Master branch. These submodules wont be managed as separate projects with separate releases/tags at this time.
## Summary
There are 3 types of branches: Master, Candidate, and Release. All feature development happens on Master. Stabilization happens on Candidate. Hotfixes happen on Release. Each product gets its own Candidate and Release branches. All candidates and releases get tagged.

View File

@ -0,0 +1,30 @@
# NetNewsWire Continuous Integration
CI for NetNewsWire is enabled through CircleCI, hosted at
<https://circleci.com/gh/brentsimmons/NetNewsWire>. The CI configuration (hosted in
[`.circleci/config.yml`](https://github.com/brentsimmons/NetNewsWire/blob/master/.circleci/config.yml)
uses `xcodebuild` to build the project after syncing the repository and
the various submodules.
As of June 2019, CircleCI offered Xcode 10.2.1, so IOS 13 and Catalina support are not available
via CI as yet.
The build itself focuses on the scheme NetNewsWire and leverages the
`NetNewsWire.xcworkspace` configuration.
Each submodule also has it's own CI configuration, which are set up and built from
their own repositories. The submodule CI systems are entirely independent so that
those libraries can grow and change, getting CI verification, indepdent of NetNewsWire.
The submodule CI are typically set to run a build and any available tests. Refer to the
project repository for the current and complete list of submodules, but for quick reference:
- [RSCore](https://github.com/brentsimmons/RSCore) [![CircleCI](https://circleci.com/gh/brentsimmons/RSCore.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSCore)
- [RSWeb](https://github.com/brentsimmons/RSWeb) [![CircleCI](https://circleci.com/gh/brentsimmons/RSWeb.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSWeb)
- [RSParser](https://github.com/brentsimmons/RSParser) [![CircleCI](https://circleci.com/gh/brentsimmons/RSParser.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSParser)
- [RSTree](https://github.com/brentsimmons/RSTree) [![CircleCI](https://circleci.com/gh/brentsimmons/RSTree.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSTree)
- [RSDatabase](https://github.com/brentsimmons/RSDatabase) [![CircleCI](https://circleci.com/gh/brentsimmons/RSDatabase.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSDatabase)

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
Technotes/Images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -16,4 +16,8 @@
## Contributing
[Contributing](../CONTRIBUTING.md)
[Coding Guidelines](CodingGuidelines.md)
[Branching Strategy](BranchingStrategy.md)

39
Technotes/Reruns.md Normal file
View File

@ -0,0 +1,39 @@
# Why Reruns Happen
Sometimes you might see a new article in a feed that youd swear youve already read. And maybe you can even see, in NetNewsWire, what looks like another copy of that same exact article, with no changes.
Heres the thing to know: if the article really was the exact same in every respect, NetNewsWire would see that. Its super-easy for a computer to tell that some data is the exact same as some other data.
When its not really the exact same, thats where the problem comes in.
Here are some reasons this situation can happen:
## A blog changes its blog engine
If someone switches from (for instance) Ghost to WordPress, then the code that creates that feeds will be different. And that code will make a different choice for the unique ID for each article in the feed.
Those unique IDs are critical: theyre how NetNewsWire identifies an article. If an article appears with a new unique ID, then NetNewsWire treats it like a new article.
In this situation, youll often see that you get a bunch of reruns for a given feed all at once. Youll get 10 or 20 or whatever.
This is by far the most common cause of reruns.
## A feed that lacks unique IDs does something weird
This is quite a bit less common. There are some feeds that dont have unique IDs, which means NetNewsWire has to use some combination of other article metadata to identify articles.
That metadata could change just enough to throw NetNewsWire off. This is rare, but it can happen.
## A feed just has terrible bugs
Weve seen feeds that create a different unique ID for each article every time you fetch the feed, which results in reruns every single time. Weve seen feeds that use the same unique ID for every article in the feed, even — which goes against the very idea of unique IDs!
Some feeds just have bugs, and weird, unpredictable things happen.
NetNewsWire is designed to be resistant to that, and it does a good job — but we havent anticipated every odd case.
However, this is the most rare cause of reruns. The most common cause is, by far, the first one: the feed is now being generated by different software.
## Reporting Bugs
If you have a feed that keeps showing reruns (as opposed to once, when a blog changes its blogging system), please do report a bug, either on our [Issues Tracker](https://github.com/brentsimmons/NetNewsWire/issues) or on the [Slack group](https://join.slack.com/t/netnewswire/shared_invite/enQtNjM4MDA1MjQzMDkzLTNlNjBhOWVhYzdhYjA4ZWFhMzQ1MTUxYjU0NTE5ZGY0YzYwZWJhNjYwNTNmNTg2NjIwYWY4YzhlYzk5NmU3ZTc).

View File

@ -394,7 +394,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
@IBAction func settings(_ sender: UIBarButtonItem) {
let settings = UIHostingController(rootView: SettingsView(viewModel: SettingsViewModel()))
let settings = UIHostingController(rootView: SettingsView(viewModel: SettingsView.ViewModel()))
self.present(settings, animated: true)
}

View File

@ -7,14 +7,15 @@
//
import SwiftUI
import Account
struct SettingsAddAccountView : View {
var body: some View {
List {
PresentationButton(SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: "On My Device"),
destination: SettingsLocalAccountView(name: ""))
PresentationButton(SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName),
destination: SettingsLocalAccountView(name: "")).padding(.all, 4)
PresentationButton(SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin"),
destination: SettingsFeedbinAccountView(email: "", password: ""))
destination: SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())).padding(.all, 4)
}
.listStyle(.grouped)
.navigationBarTitle(Text("Add Account"), displayMode: .inline)

View File

@ -0,0 +1,114 @@
//
// SettingsDetailAccountView.swift
// NetNewsWire
//
// Created by Maurice Parker on 6/13/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import SwiftUI
import Combine
import Account
struct SettingsDetailAccountView : View {
@ObjectBinding var viewModel: ViewModel
@State private var verifyDelete = false
var body: some View {
List {
Section {
HStack {
Text("Name")
Divider()
TextField($viewModel.name, placeholder: Text("(Optional)"))
}
Toggle(isOn: $viewModel.isActive) {
Text("Active")
}
}
Section {
HStack {
Spacer()
Button(action: {
}) {
Text("Credentials")
}
Spacer()
}
}
if viewModel.isDeletable {
Section {
HStack {
Spacer()
Button(action: {
self.verifyDelete = true
}) {
Text("Delete Account")
.foregroundColor(.red)
}
.presentation($verifyDelete) {
Alert(title: Text("Are you sure you want to delete \"\(viewModel.nameForDisplay)\"?"),
primaryButton: Alert.Button.default(Text("Delete"), onTrigger: { self.viewModel.delete() }),
secondaryButton: Alert.Button.cancel())
}
Spacer()
}
}
}
}
.listStyle(.grouped)
.navigationBarTitle(Text(verbatim: viewModel.nameForDisplay), displayMode: .inline)
}
class ViewModel: BindableObject {
let didChange = PassthroughSubject<ViewModel, Never>()
let account: Account
init(_ account: Account) {
self.account = account
}
var nameForDisplay: String {
account.nameForDisplay
}
var name: String {
get {
account.name ?? ""
}
set {
account.name = newValue.isEmpty ? nil : newValue
didChange.send(self)
}
}
var isActive: Bool {
get {
account.isActive
}
set {
account.isActive = newValue
didChange.send(self)
}
}
var isDeletable: Bool {
return AccountManager.shared.defaultAccount != account
}
func delete() {
AccountManager.shared.deleteAccount(account)
}
}
}
#if DEBUG
struct SettingsDetailAccountView_Previews : PreviewProvider {
static var previews: some View {
let viewModel = SettingsDetailAccountView.ViewModel(AccountManager.shared.defaultAccount)
return SettingsDetailAccountView(viewModel: viewModel)
}
}
#endif

View File

@ -7,11 +7,17 @@
//
import SwiftUI
import Combine
import Account
import RSWeb
struct SettingsFeedbinAccountView : View {
@State var email: String
@State var password: String
@Environment(\.isPresented) private var isPresented
@ObjectBinding var viewModel: ViewModel
@State var busy: Bool = false
@State var error: Text = Text("")
var account: Account? = nil
var body: some View {
NavigationView {
List {
@ -20,27 +26,116 @@ struct SettingsFeedbinAccountView : View {
) {
HStack {
Spacer()
TextField($email, placeholder: Text("Email"))
TextField($viewModel.email, placeholder: Text("Email"))
.textContentType(.username)
Spacer()
}
HStack {
Spacer()
SecureField($password, placeholder: Text("Password"))
SecureField($viewModel.password, placeholder: Text("Password"))
Spacer()
}
}
Section {
Section(footer:
HStack {
Spacer()
Button(action: {}) {
error.color(.red)
Spacer()
}
) {
HStack {
Spacer()
Button(action: { self.addAccount() }) {
Text("Add Account")
}
.disabled(!viewModel.isValid)
Spacer()
}
}
}
.disabled(busy)
.listStyle(.grouped)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading:
Button(action: { self.dismiss() }) { Text("Cancel") }
)
}
}
private func addAccount() {
busy = true
let emailAddress = viewModel.email.trimmingCharacters(in: .whitespaces)
let credentials = Credentials.basic(username: emailAddress, password: viewModel.password)
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
self.busy = false
switch result {
case .success(let authenticated):
if authenticated {
var newAccount = false
let workAccount: Account
if self.account == nil {
workAccount = AccountManager.shared.createAccount(type: .feedbin)
newAccount = true
} else {
workAccount = self.account!
}
do {
do {
try workAccount.removeBasicCredentials()
} catch {}
try workAccount.storeCredentials(credentials)
if newAccount {
workAccount.refreshAll() { result in }
}
self.dismiss()
} catch {
self.error = Text("Keychain error while storing credentials.")
}
} else {
self.error = Text("Invalid email/password combination.")
}
case .failure:
self.error = Text("Network error. Try again later.")
}
}
}
private func dismiss() {
isPresented?.value = false
}
class ViewModel: BindableObject {
let didChange = PassthroughSubject<ViewModel, Never>()
var email: String = "" {
didSet {
didChange.send(self)
}
}
var password: String = "" {
didSet {
didChange.send(self)
}
}
var isValid: Bool {
return !email.isEmpty && !password.isEmpty
}
}
@ -49,7 +144,7 @@ struct SettingsFeedbinAccountView : View {
#if DEBUG
struct SettingsFeedbinAccountView_Previews : PreviewProvider {
static var previews: some View {
SettingsFeedbinAccountView(email: "", password: "")
SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())
}
}
#endif

View File

@ -10,13 +10,14 @@ import SwiftUI
import Account
struct SettingsLocalAccountView : View {
@Environment(\.isPresented) private var isPresented
@State var name: String
var body: some View {
NavigationView {
List {
Section(header:
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: "On My Device").padding()
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName).padding()
) {
HStack {
Spacer()
@ -36,13 +37,20 @@ struct SettingsLocalAccountView : View {
}
.listStyle(.grouped)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading: Button(action: { self.dismiss() }) { Text("Cancel") } )
}
}
func addAccount() {
private func addAccount() {
let account = AccountManager.shared.createAccount(type: .onMyMac)
account.name = name
dismiss()
}
private func dismiss() {
isPresented?.value = false
}
}
#if DEBUG

View File

@ -7,10 +7,11 @@
//
import SwiftUI
import Combine
import Account
struct SettingsView : View {
@ObjectBinding var viewModel: SettingsViewModel
@ObjectBinding var viewModel: ViewModel
var body: some View {
NavigationView {
@ -18,7 +19,9 @@ struct SettingsView : View {
Section(header: Text("ACCOUNTS")) {
ForEach(viewModel.accounts.identified(by: \.self)) { account in
Text(verbatim: account.nameForDisplay)
NavigationButton(destination: SettingsDetailAccountView(viewModel: SettingsDetailAccountView.ViewModel(account)), isDetail: false) {
Text(verbatim: account.nameForDisplay)
}
}
NavigationButton(destination: SettingsAddAccountView(), isDetail: false) {
Text("Add Account")
@ -83,12 +86,74 @@ struct SettingsView : View {
}
}
class ViewModel: BindableObject {
let didChange = PassthroughSubject<ViewModel, Never>()
init() {
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
}
var accounts: [Account] {
get {
return AccountManager.shared.sortedAccounts
}
set {
}
}
var sortOldestToNewest: Bool {
get {
return AppDefaults.timelineSortDirection == .orderedDescending
}
set {
if newValue == true {
AppDefaults.timelineSortDirection = .orderedDescending
} else {
AppDefaults.timelineSortDirection = .orderedAscending
}
didChange.send(self)
}
}
var timelineNumberOfLines: Int {
get {
return AppDefaults.timelineNumberOfLines
}
set {
AppDefaults.timelineNumberOfLines = newValue
didChange.send(self)
}
}
var refreshInterval: RefreshInterval {
get {
return AppDefaults.refreshInterval
}
set {
AppDefaults.refreshInterval = newValue
didChange.send(self)
}
}
@objc func accountsDidChange(_ notification: Notification) {
didChange.send(self)
}
@objc func displayNameDidChange(_ notification: Notification) {
didChange.send(self)
}
}
}
#if DEBUG
struct SettingsView_Previews : PreviewProvider {
static var previews: some View {
SettingsView(viewModel: SettingsViewModel())
SettingsView(viewModel: SettingsView.ViewModel())
}
}
#endif

View File

@ -1,68 +0,0 @@
//
// SettingsViewModel.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 6/11/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import SwiftUI
import Combine
import Account
class SettingsViewModel: BindableObject {
let didChange = PassthroughSubject<SettingsViewModel, Never>()
init() {
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil)
}
var accounts: [Account] {
get {
return AccountManager.shared.accounts
}
set {
}
}
var sortOldestToNewest: Bool {
get {
return AppDefaults.timelineSortDirection == .orderedDescending
}
set {
if newValue == true {
AppDefaults.timelineSortDirection = .orderedDescending
} else {
AppDefaults.timelineSortDirection = .orderedAscending
}
didChange.send(self)
}
}
var timelineNumberOfLines: Int {
get {
return AppDefaults.timelineNumberOfLines
}
set {
AppDefaults.timelineNumberOfLines = newValue
didChange.send(self)
}
}
var refreshInterval: RefreshInterval {
get {
return AppDefaults.refreshInterval
}
set {
AppDefaults.refreshInterval = newValue
didChange.send(self)
}
}
@objc func accountsDidChange(_ notification: Notification) {
didChange.send(self)
}
}

@ -1 +1 @@
Subproject commit 812594e902d4d640b08fea9cc6184d6ee84424e2
Subproject commit 032edf89b64ccbbfb6c05887b239a4bf81329b92

@ -1 +1 @@
Subproject commit 1686ee1b1c5a4aef6ebe1ffd72b17d6defabd9fd
Subproject commit 5d648e4050b700bb20fc7ae3303f087edcb3228f