Refactoring

This commit is contained in:
Justin Mazzocchi 2020-09-07 19:12:38 -07:00
parent 367d79ed2c
commit b4549521cb
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
40 changed files with 282 additions and 369 deletions

View File

@ -14,7 +14,7 @@ public enum IdentityDatabaseError: Error {
public struct IdentityDatabase { public struct IdentityDatabase {
private let databaseQueue: DatabaseQueue private let databaseQueue: DatabaseQueue
public init(inMemory: Bool, fixture: IdentityFixture?, keychain: Keychain.Type) throws { public init(inMemory: Bool, keychain: Keychain.Type) throws {
if inMemory { if inMemory {
databaseQueue = DatabaseQueue() databaseQueue = DatabaseQueue()
} else { } else {
@ -29,10 +29,6 @@ public struct IdentityDatabase {
} }
try Self.migrate(databaseQueue) try Self.migrate(databaseQueue)
if let fixture = fixture {
try populate(fixture: fixture)
}
} }
} }
@ -262,22 +258,4 @@ private extension IdentityDatabase {
try migrator.migrate(writer) try migrator.migrate(writer)
} }
func populate(fixture: IdentityFixture) throws {
_ = createIdentity(id: fixture.id, url: fixture.instanceURL)
.receive(on: ImmediateScheduler.shared)
.sink { _ in } receiveValue: { _ in }
if let instance = fixture.instance {
_ = updateInstance(instance, forIdentityID: fixture.id)
.receive(on: ImmediateScheduler.shared)
.sink { _ in } receiveValue: { _ in }
}
if let account = fixture.account {
_ = updateAccount(account, forIdentityID: fixture.id)
.receive(on: ImmediateScheduler.shared)
.sink { _ in } receiveValue: { _ in }
}
}
} }

View File

@ -6,9 +6,6 @@ import Stubbing
extension AccountEndpoint: Stubbing { extension AccountEndpoint: Stubbing {
public func data(url: URL) -> Data? { public func data(url: URL) -> Data? {
switch self { StubData.account
case .verifyCredentials: return try? Data(contentsOf: Bundle.module.url(forResource: "account",
withExtension: "json")!)
}
} }
} }

View File

@ -6,8 +6,6 @@ import Stubbing
extension InstanceEndpoint: Stubbing { extension InstanceEndpoint: Stubbing {
public func data(url: URL) -> Data? { public func data(url: URL) -> Data? {
switch self { StubData.instance
case .instance: return try? Data(contentsOf: Bundle.module.url(forResource: "instance", withExtension: "json")!)
}
} }
} }

View File

@ -5,18 +5,7 @@ import MastodonAPI
import Stubbing import Stubbing
extension PreferencesEndpoint: Stubbing { extension PreferencesEndpoint: Stubbing {
public func dataString(url: URL) -> String? { public func data(url: URL) -> Data? {
switch self { StubData.preferences
case .preferences:
return """
{
"posting:default:visibility": "public",
"posting:default:sensitive": false,
"posting:default:language": null,
"reading:expand:media": "default",
"reading:expand:spoilers": false
}
"""
}
} }
} }

View File

@ -0,0 +1,7 @@
{
"posting:default:visibility": "public",
"posting:default:sensitive": false,
"posting:default:language": null,
"reading:expand:media": "default",
"reading:expand:spoilers": false
}

View File

@ -0,0 +1,18 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
public enum StubData {}
public extension StubData {
// swiftlint:disable force_try
static let account = try! Data(contentsOf: Bundle.module.url(forResource: "account",
withExtension: "json")!)
static let instance = try! Data(contentsOf: Bundle.module.url(forResource: "instance",
withExtension: "json")!)
static let preferences = try! Data(contentsOf: Bundle.module.url(forResource: "preferences",
withExtension: "json")!)
static let timeline = try! Data(contentsOf: Bundle.module.url(forResource: "timeline",
withExtension: "json")!)
// swiftlint:enable force_try
}

View File

@ -6,6 +6,6 @@ import Stubbing
extension TimelinesEndpoint: Stubbing { extension TimelinesEndpoint: Stubbing {
public func data(url: URL) -> Data? { public func data(url: URL) -> Data? {
try? Data(contentsOf: Bundle.module.url(forResource: "timeline", withExtension: "json")!) StubData.timeline
} }
} }

View File

@ -25,6 +25,7 @@ public struct Secrets {
public extension Secrets { public extension Secrets {
enum Item: String, CaseIterable { enum Item: String, CaseIterable {
case instanceURL
case clientID case clientID
case clientSecret case clientSecret
case accessToken case accessToken
@ -101,6 +102,14 @@ public extension Secrets {
} }
} }
func getInstanceURL() throws -> URL {
try item(.instanceURL)
}
func setInstanceURL(_ instanceURL: URL) throws {
try set(instanceURL, forItem: .instanceURL)
}
func getClientID() throws -> String { func getClientID() throws -> String {
try item(.clientID) try item(.clientID)
} }
@ -219,6 +228,18 @@ extension String: SecretsStorable {
} }
} }
extension URL: SecretsStorable {
public var dataStoredInSecrets: Data { absoluteString.dataStoredInSecrets }
public static func fromDataStoredInSecrets(_ data: Data) throws -> URL {
guard let url = URL(string: try String.fromDataStoredInSecrets(data)) else {
throw SecretsStorableError.conversionFromDataStoredInSecrets(data)
}
return url
}
}
private struct PushKey { private struct PushKey {
static let authLength = 16 static let authLength = 16
static let sizeInBits = 256 static let sizeInBits = 256

View File

@ -14,7 +14,7 @@ public struct AppEnvironment {
let userDefaults: UserDefaults let userDefaults: UserDefaults
let userNotificationClient: UserNotificationClient let userNotificationClient: UserNotificationClient
let inMemoryContent: Bool let inMemoryContent: Bool
let identityFixture: IdentityFixture? let fixtureDatabase: IdentityDatabase?
public init(session: Session, public init(session: Session,
webAuthSessionType: WebAuthSession.Type, webAuthSessionType: WebAuthSession.Type,
@ -22,14 +22,14 @@ public struct AppEnvironment {
userDefaults: UserDefaults, userDefaults: UserDefaults,
userNotificationClient: UserNotificationClient, userNotificationClient: UserNotificationClient,
inMemoryContent: Bool, inMemoryContent: Bool,
identityFixture: IdentityFixture?) { fixtureDatabase: IdentityDatabase?) {
self.session = session self.session = session
self.webAuthSessionType = webAuthSessionType self.webAuthSessionType = webAuthSessionType
self.keychain = keychain self.keychain = keychain
self.userDefaults = userDefaults self.userDefaults = userDefaults
self.userNotificationClient = userNotificationClient self.userNotificationClient = userNotificationClient
self.inMemoryContent = inMemoryContent self.inMemoryContent = inMemoryContent
self.identityFixture = identityFixture self.fixtureDatabase = fixtureDatabase
} }
} }
@ -42,6 +42,6 @@ public extension AppEnvironment {
userDefaults: .standard, userDefaults: .standard,
userNotificationClient: .live(userNotificationCenter), userNotificationClient: .live(userNotificationCenter),
inMemoryContent: false, inMemoryContent: false,
identityFixture: nil) fixtureDatabase: nil)
} }
} }

View File

@ -1,43 +0,0 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import DB
import Foundation
public class IdentifiedEnvironment {
@Published public private(set) var identity: Identity
public let appEnvironment: AppEnvironment
public let identityService: IdentityService
public let observationErrors: AnyPublisher<Error, Never>
init(id: UUID, database: IdentityDatabase, environment: AppEnvironment) throws {
appEnvironment = environment
// The scheduling on the observation is immediate so an initial value can be extracted
let sharedObservation = database.identityObservation(id: id).share()
var initialIdentity: Identity?
_ = sharedObservation.first().sink(
receiveCompletion: { _ in },
receiveValue: { initialIdentity = $0 })
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
self.identity = identity
identityService = try IdentityService(id: identity.id,
instanceURL: identity.url,
database: database,
environment: environment)
let observationErrorsSubject = PassthroughSubject<Error, Never>()
self.observationErrors = observationErrorsSubject.eraseToAnyPublisher()
sharedObservation.catch { error -> Empty<Identity, Never> in
observationErrorsSubject.send(error)
return Empty()
}
.assign(to: &$identity)
}
}

View File

@ -15,8 +15,8 @@ public struct AllIdentitiesService {
private let environment: AppEnvironment private let environment: AppEnvironment
public init(environment: AppEnvironment) throws { public init(environment: AppEnvironment) throws {
self.database = try IdentityDatabase(inMemory: environment.inMemoryContent, self.database = try environment.fixtureDatabase ?? IdentityDatabase(
fixture: environment.identityFixture, inMemory: environment.inMemoryContent,
keychain: environment.keychain) keychain: environment.keychain)
self.environment = environment self.environment = environment
@ -28,8 +28,8 @@ public struct AllIdentitiesService {
} }
public extension AllIdentitiesService { public extension AllIdentitiesService {
func identifiedEnvironment(id: UUID) throws -> IdentifiedEnvironment { func identityService(id: UUID) throws -> IdentityService {
try IdentifiedEnvironment(id: id, database: database, environment: environment) try IdentityService(id: id, database: database, environment: environment)
} }
func createIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> { func createIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> {
@ -42,6 +42,7 @@ public extension AllIdentitiesService {
return authenticationService.authorizeApp(instanceURL: instanceURL) return authenticationService.authorizeApp(instanceURL: instanceURL)
.tryMap { appAuthorization -> (URL, AppAuthorization) in .tryMap { appAuthorization -> (URL, AppAuthorization) in
try secrets.setInstanceURL(instanceURL)
try secrets.setClientID(appAuthorization.clientId) try secrets.setClientID(appAuthorization.clientId)
try secrets.setClientSecret(appAuthorization.clientSecret) try secrets.setClientSecret(appAuthorization.clientSecret)
@ -81,7 +82,7 @@ public extension AllIdentitiesService {
database.identitiesWithOutdatedDeviceTokens(deviceToken: deviceToken) database.identitiesWithOutdatedDeviceTokens(deviceToken: deviceToken)
.tryMap { identities -> [AnyPublisher<Never, Never>] in .tryMap { identities -> [AnyPublisher<Never, Never>] in
try identities.map { try identities.map {
try IdentityService(id: $0.id, instanceURL: $0.url, database: database, environment: environment) try IdentityService(id: $0.id, database: database, environment: environment)
.createPushSubscription(deviceToken: deviceToken, alerts: $0.pushSubscriptionAlerts) .createPushSubscription(deviceToken: deviceToken, alerts: $0.pushSubscriptionAlerts)
.catch { _ in Empty() } // don't want to disrupt pipeline .catch { _ in Empty() } // don't want to disrupt pipeline
.eraseToAnyPublisher() .eraseToAnyPublisher()

View File

@ -17,7 +17,7 @@ public struct IdentityService {
private let secrets: Secrets private let secrets: Secrets
private let observationErrorsInput = PassthroughSubject<Error, Never>() private let observationErrorsInput = PassthroughSubject<Error, Never>()
init(id: UUID, instanceURL: URL, database: IdentityDatabase, environment: AppEnvironment) throws { init(id: UUID, database: IdentityDatabase, environment: AppEnvironment) throws {
identityID = id identityID = id
identityDatabase = database identityDatabase = database
self.environment = environment self.environment = environment
@ -25,7 +25,7 @@ public struct IdentityService {
identityID: id, identityID: id,
keychain: environment.keychain) keychain: environment.keychain)
mastodonAPIClient = MastodonAPIClient(session: environment.session) mastodonAPIClient = MastodonAPIClient(session: environment.session)
mastodonAPIClient.instanceURL = instanceURL mastodonAPIClient.instanceURL = try secrets.getInstanceURL()
mastodonAPIClient.accessToken = try? secrets.getAccessToken() mastodonAPIClient.accessToken = try? secrets.getAccessToken()
contentDatabase = try ContentDatabase(identityID: id, contentDatabase = try ContentDatabase(identityID: id,
@ -86,6 +86,10 @@ public extension IdentityService {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func observation() -> AnyPublisher<Identity, Error> {
identityDatabase.identityObservation(id: identityID)
}
func listsObservation() -> AnyPublisher<[Timeline], Error> { func listsObservation() -> AnyPublisher<[Timeline], Error> {
contentDatabase.listsObservation() contentDatabase.listsObservation()
} }

View File

@ -3,12 +3,19 @@
import DB import DB
import Foundation import Foundation
import HTTP import HTTP
import Keychain
import MockKeychain import MockKeychain
import ServiceLayer import ServiceLayer
import Stubbing import Stubbing
public extension AppEnvironment { public extension AppEnvironment {
static func mock(identityFixture: IdentityFixture? = nil) -> Self { static func mock(session: Session = Session(configuration: .stubbing),
webAuthSessionType: WebAuthSession.Type = SuccessfulMockWebAuthSession.self,
keychain: Keychain.Type = MockKeychain.self,
userDefaults: UserDefaults = MockUserDefaults(),
userNotificationClient: UserNotificationClient = .mock,
inMemoryContent: Bool = true,
fixtureDatabase: IdentityDatabase? = nil) -> Self {
AppEnvironment( AppEnvironment(
session: Session(configuration: .stubbing), session: Session(configuration: .stubbing),
webAuthSessionType: SuccessfulMockWebAuthSession.self, webAuthSessionType: SuccessfulMockWebAuthSession.self,
@ -16,6 +23,6 @@ public extension AppEnvironment {
userDefaults: MockUserDefaults(), userDefaults: MockUserDefaults(),
userNotificationClient: .mock, userNotificationClient: .mock,
inMemoryContent: true, inMemoryContent: true,
identityFixture: identityFixture) fixtureDatabase: fixtureDatabase)
} }
} }

View File

@ -0,0 +1,53 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import DB
import Foundation
import Mastodon
import MastodonAPIStubs
import MockKeychain
import Secrets
import ServiceLayer
import ServiceLayerMocks
import ViewModels
// swiftlint:disable force_try
let db: IdentityDatabase = {
let id = UUID()
let url = URL(string: "https://mastodon.social")!
let db = try! IdentityDatabase(inMemory: true, keychain: MockKeychain.self)
let decoder = MastodonDecoder()
let instance = try! decoder.decode(Instance.self, from: StubData.instance)
let account = try! decoder.decode(Account.self, from: StubData.account)
let secrets = Secrets(identityID: id, keychain: MockKeychain.self)
try! secrets.setInstanceURL(url)
try! secrets.setAccessToken(UUID().uuidString)
_ = db.createIdentity(id: id, url: url)
.receive(on: ImmediateScheduler.shared)
.sink { _ in } receiveValue: { _ in }
_ = db.updateInstance(instance, forIdentityID: id)
.receive(on: ImmediateScheduler.shared)
.sink { _ in } receiveValue: { _ in }
_ = db.updateAccount(account, forIdentityID: id)
.receive(on: ImmediateScheduler.shared)
.sink { _ in } receiveValue: { _ in }
return db
}()
let environment = AppEnvironment.mock(fixtureDatabase: db)
public extension RootViewModel {
static let preview = try! RootViewModel(environment: environment) { Empty().eraseToAnyPublisher() }
}
public extension Identification {
static let preview = RootViewModel.preview.identification!
}
// swiftlint:enable force_try

View File

@ -1,104 +0,0 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import Foundation
import HTTP
import Mastodon
import MastodonAPI
import MastodonAPIStubs
import ServiceLayer
import ServiceLayerMocks
import ViewModels
private let decoder = MastodonDecoder()
private let devInstanceURL = URL(string: "https://mastodon.social")!
// swiftlint:disable force_try
extension AppEnvironment {
public static let mockAuthenticated: Self = .mock(
identityFixture: .init(
id: UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!,
instanceURL: devInstanceURL,
instance: try! decoder.decode(Instance.self,
from: InstanceEndpoint.instance.data(url: devInstanceURL)!),
account: try! decoder.decode(Account.self,
from: AccountEndpoint.verifyCredentials.data(url: devInstanceURL)!)))
}
extension RootViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> Self {
try! Self(environment: environment,
registerForRemoteNotifications: { Empty().eraseToAnyPublisher() })
}
}
// swiftlint:enable force_try
extension AddIdentityViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> AddIdentityViewModel {
RootViewModel.mock(environment: environment).addIdentityViewModel()
}
}
extension TabNavigationViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> TabNavigationViewModel {
RootViewModel.mock(environment: environment).tabNavigationViewModel!
}
}
extension SecondaryNavigationViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> SecondaryNavigationViewModel {
TabNavigationViewModel.mock(environment: environment)
.secondaryNavigationViewModel()
}
}
extension IdentitiesViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> IdentitiesViewModel {
SecondaryNavigationViewModel.mock(environment: environment).identitiesViewModel()
}
}
extension ListsViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> ListsViewModel {
SecondaryNavigationViewModel.mock(environment: environment).listsViewModel()
}
}
extension PreferencesViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> PreferencesViewModel {
SecondaryNavigationViewModel.mock(environment: environment).preferencesViewModel()
}
}
extension PostingReadingPreferencesViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> PostingReadingPreferencesViewModel {
PreferencesViewModel.mock(environment: environment)
.postingReadingPreferencesViewModel()
}
}
extension NotificationTypesPreferencesViewModel {
public static func mock(
environment: AppEnvironment = .mockAuthenticated) -> NotificationTypesPreferencesViewModel {
PreferencesViewModel.mock(environment: environment)
.notificationTypesPreferencesViewModel()
}
}
extension FiltersViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> FiltersViewModel {
PreferencesViewModel.mock(environment: environment).filtersViewModel()
}
}
extension EditFilterViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> EditFilterViewModel {
FiltersViewModel.mock(environment: environment).editFilterViewModel(filter: .new)
}
}
extension StatusListViewModel {
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> StatusListViewModel {
TabNavigationViewModel.mock(environment: environment).viewModel(timeline: .home)
}
}

View File

@ -15,13 +15,13 @@ public class EditFilterViewModel: ObservableObject {
didSet { filter.expiresAt = date } didSet { filter.expiresAt = date }
} }
private let environment: IdentifiedEnvironment private let identification: Identification
private let saveCompletedInput = PassthroughSubject<Void, Never>() private let saveCompletedInput = PassthroughSubject<Void, Never>()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(filter: Filter, environment: IdentifiedEnvironment) { public init(filter: Filter, identification: Identification) {
self.filter = filter self.filter = filter
self.environment = environment self.identification = identification
date = filter.expiresAt ?? Date() date = filter.expiresAt ?? Date()
saveCompleted = saveCompletedInput.eraseToAnyPublisher() saveCompleted = saveCompletedInput.eraseToAnyPublisher()
} }
@ -41,7 +41,7 @@ public extension EditFilterViewModel {
} }
func save() { func save() {
(isNew ? environment.identityService.createFilter(filter) : environment.identityService.updateFilter(filter)) (isNew ? identification.service.createFilter(filter) : identification.service.updateFilter(filter))
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.handleEvents( .handleEvents(
receiveSubscription: { [weak self] _ in self?.saving = true }, receiveSubscription: { [weak self] _ in self?.saving = true },

View File

@ -0,0 +1,41 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import Foundation
import ServiceLayer
enum IdentificationError: Error {
case initialIdentityValueAbsent
}
public final class Identification: ObservableObject {
@Published private(set) var identity: Identity
let service: IdentityService
let observationErrors: AnyPublisher<Error, Never>
init(service: IdentityService) throws {
self.service = service
// The scheduling on the observation is immediate so an initial value can be extracted
let sharedObservation = service.observation().share()
var initialIdentity: Identity?
_ = sharedObservation.first().sink(
receiveCompletion: { _ in },
receiveValue: { initialIdentity = $0 })
guard let identity = initialIdentity else { throw IdentificationError.initialIdentityValueAbsent }
self.identity = identity
let observationErrorsSubject = PassthroughSubject<Error, Never>()
observationErrors = observationErrorsSubject.eraseToAnyPublisher()
sharedObservation.catch { error -> Empty<Identity, Never> in
observationErrorsSubject.send(error)
return Empty()
}
.assign(to: &$identity)
}
}

View File

@ -10,19 +10,19 @@ public class FiltersViewModel: ObservableObject {
@Published public var expiredFilters = [Filter]() @Published public var expiredFilters = [Filter]()
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
private let environment: IdentifiedEnvironment private let identification: Identification
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(environment: IdentifiedEnvironment) { public init(identification: Identification) {
self.environment = environment self.identification = identification
let now = Date() let now = Date()
environment.identityService.activeFiltersObservation(date: now) identification.service.activeFiltersObservation(date: now)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$activeFilters) .assign(to: &$activeFilters)
environment.identityService.expiredFiltersObservation(date: now) identification.service.expiredFiltersObservation(date: now)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$expiredFilters) .assign(to: &$expiredFilters)
} }
@ -30,20 +30,16 @@ public class FiltersViewModel: ObservableObject {
public extension FiltersViewModel { public extension FiltersViewModel {
func refreshFilters() { func refreshFilters() {
environment.identityService.refreshFilters() identification.service.refreshFilters()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }
func delete(filter: Filter) { func delete(filter: Filter) {
environment.identityService.deleteFilter(id: filter.id) identification.service.deleteFilter(id: filter.id)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }
func editFilterViewModel(filter: Filter) -> EditFilterViewModel {
EditFilterViewModel(filter: filter, environment: environment)
}
} }

View File

@ -9,14 +9,14 @@ public class IdentitiesViewModel: ObservableObject {
@Published public var identities = [Identity]() @Published public var identities = [Identity]()
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
private let environment: IdentifiedEnvironment private let identification: Identification
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(environment: IdentifiedEnvironment) { public init(identification: Identification) {
self.environment = environment self.identification = identification
currentIdentityID = environment.identity.id currentIdentityID = identification.identity.id
environment.identityService.identitiesObservation() identification.service.identitiesObservation()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$identities) .assign(to: &$identities)
} }

View File

@ -10,13 +10,13 @@ public class ListsViewModel: ObservableObject {
@Published public private(set) var creatingList = false @Published public private(set) var creatingList = false
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
private let environment: IdentifiedEnvironment private let identification: Identification
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(environment: IdentifiedEnvironment) { public init(identification: Identification) {
self.environment = environment self.identification = identification
environment.identityService.listsObservation() identification.service.listsObservation()
.map { .map {
$0.compactMap { $0.compactMap {
guard case let .list(list) = $0 else { return nil } guard case let .list(list) = $0 else { return nil }
@ -31,14 +31,14 @@ public class ListsViewModel: ObservableObject {
public extension ListsViewModel { public extension ListsViewModel {
func refreshLists() { func refreshLists() {
environment.identityService.refreshLists() identification.service.refreshLists()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }
func createList(title: String) { func createList(title: String) {
environment.identityService.createList(title: title) identification.service.createList(title: title)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.handleEvents( .handleEvents(
receiveSubscription: { [weak self] _ in self?.creatingList = true }, receiveSubscription: { [weak self] _ in self?.creatingList = true },
@ -48,7 +48,7 @@ public extension ListsViewModel {
} }
func delete(list: MastodonList) { func delete(list: MastodonList) {
environment.identityService.deleteList(id: list.id) identification.service.deleteList(id: list.id)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)

View File

@ -9,14 +9,14 @@ public class NotificationTypesPreferencesViewModel: ObservableObject {
@Published public var pushSubscriptionAlerts: PushSubscription.Alerts @Published public var pushSubscriptionAlerts: PushSubscription.Alerts
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
private let environment: IdentifiedEnvironment private let identification: Identification
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(environment: IdentifiedEnvironment) { public init(identification: Identification) {
self.environment = environment self.identification = identification
pushSubscriptionAlerts = environment.identity.pushSubscriptionAlerts pushSubscriptionAlerts = identification.identity.pushSubscriptionAlerts
environment.$identity identification.$identity
.map(\.pushSubscriptionAlerts) .map(\.pushSubscriptionAlerts)
.dropFirst() .dropFirst()
.removeDuplicates() .removeDuplicates()
@ -32,14 +32,14 @@ public class NotificationTypesPreferencesViewModel: ObservableObject {
private extension NotificationTypesPreferencesViewModel { private extension NotificationTypesPreferencesViewModel {
func update(alerts: PushSubscription.Alerts) { func update(alerts: PushSubscription.Alerts) {
guard alerts != environment.identity.pushSubscriptionAlerts else { return } guard alerts != identification.identity.pushSubscriptionAlerts else { return }
environment.identityService.updatePushSubscription(alerts: alerts) identification.service.updatePushSubscription(alerts: alerts)
.sink { [weak self] in .sink { [weak self] in
guard let self = self, case let .failure(error) = $0 else { return } guard let self = self, case let .failure(error) = $0 else { return }
self.alertItem = AlertItem(error: error) self.alertItem = AlertItem(error: error)
self.pushSubscriptionAlerts = self.environment.identity.pushSubscriptionAlerts self.pushSubscriptionAlerts = self.identification.identity.pushSubscriptionAlerts
} receiveValue: { _ in } } receiveValue: { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -8,14 +8,14 @@ public class PostingReadingPreferencesViewModel: ObservableObject {
@Published public var preferences: Identity.Preferences @Published public var preferences: Identity.Preferences
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
private let environment: IdentifiedEnvironment private let identification: Identification
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(environment: IdentifiedEnvironment) { public init(identification: Identification) {
self.environment = environment self.identification = identification
preferences = environment.identity.preferences preferences = identification.identity.preferences
environment.$identity identification.$identity
.map(\.preferences) .map(\.preferences)
.dropFirst() .dropFirst()
.removeDuplicates() .removeDuplicates()
@ -23,7 +23,7 @@ public class PostingReadingPreferencesViewModel: ObservableObject {
$preferences $preferences
.dropFirst() .dropFirst()
.flatMap(environment.identityService.updatePreferences) .flatMap(identification.service.updatePreferences)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)

View File

@ -7,26 +7,12 @@ public class PreferencesViewModel: ObservableObject {
public let handle: String public let handle: String
public let shouldShowNotificationTypePreferences: Bool public let shouldShowNotificationTypePreferences: Bool
private let environment: IdentifiedEnvironment private let identification: Identification
init(environment: IdentifiedEnvironment) { public init(identification: Identification) {
self.environment = environment self.identification = identification
handle = environment.identity.handle handle = identification.identity.handle
shouldShowNotificationTypePreferences = environment.identity.lastRegisteredDeviceToken != nil shouldShowNotificationTypePreferences = identification.identity.lastRegisteredDeviceToken != nil
}
}
public extension PreferencesViewModel {
func postingReadingPreferencesViewModel() -> PostingReadingPreferencesViewModel {
PostingReadingPreferencesViewModel(environment: environment)
}
func notificationTypesPreferencesViewModel() -> NotificationTypesPreferencesViewModel {
NotificationTypesPreferencesViewModel(environment: environment)
}
func filtersViewModel() -> FiltersViewModel {
FiltersViewModel(environment: environment)
} }
} }

View File

@ -5,10 +5,9 @@ import Foundation
import ServiceLayer import ServiceLayer
public final class RootViewModel: ObservableObject { public final class RootViewModel: ObservableObject {
@Published public private(set) var tabNavigationViewModel: TabNavigationViewModel? @Published public private(set) var identification: Identification?
@Published private var mostRecentlyUsedIdentityID: UUID? @Published private var mostRecentlyUsedIdentityID: UUID?
private let environment: AppEnvironment
private let allIdentitiesService: AllIdentitiesService private let allIdentitiesService: AllIdentitiesService
private let userNotificationService: UserNotificationService private let userNotificationService: UserNotificationService
private let registerForRemoteNotifications: () -> AnyPublisher<Data, Error> private let registerForRemoteNotifications: () -> AnyPublisher<Data, Error>
@ -16,7 +15,6 @@ public final class RootViewModel: ObservableObject {
public init(environment: AppEnvironment, public init(environment: AppEnvironment,
registerForRemoteNotifications: @escaping () -> AnyPublisher<Data, Error>) throws { registerForRemoteNotifications: @escaping () -> AnyPublisher<Data, Error>) throws {
self.environment = environment
allIdentitiesService = try AllIdentitiesService(environment: environment) allIdentitiesService = try AllIdentitiesService(environment: environment)
userNotificationService = UserNotificationService(environment: environment) userNotificationService = UserNotificationService(environment: environment)
self.registerForRemoteNotifications = registerForRemoteNotifications self.registerForRemoteNotifications = registerForRemoteNotifications
@ -38,39 +36,38 @@ public final class RootViewModel: ObservableObject {
public extension RootViewModel { public extension RootViewModel {
func newIdentitySelected(id: UUID?) { func newIdentitySelected(id: UUID?) {
guard let id = id else { guard let id = id else {
tabNavigationViewModel = nil identification = nil
return return
} }
let identifiedEnvironment: IdentifiedEnvironment let identification: Identification
do { do {
identifiedEnvironment = try allIdentitiesService.identifiedEnvironment(id: id) identification = try Identification(service: allIdentitiesService.identityService(id: id))
self.identification = identification
} catch { } catch {
return return
} }
identifiedEnvironment.observationErrors identification.observationErrors
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
.map { [weak self] _ in self?.mostRecentlyUsedIdentityID } .map { [weak self] _ in self?.mostRecentlyUsedIdentityID }
.sink { [weak self] in self?.newIdentitySelected(id: $0) } .sink { [weak self] in self?.newIdentitySelected(id: $0) }
.store(in: &cancellables) .store(in: &cancellables)
identifiedEnvironment.identityService.updateLastUse() identification.service.updateLastUse()
.sink { _ in } receiveValue: { _ in } .sink { _ in } receiveValue: { _ in }
.store(in: &cancellables) .store(in: &cancellables)
userNotificationService.isAuthorized() userNotificationService.isAuthorized()
.filter { $0 } .filter { $0 }
.zip(registerForRemoteNotifications()) .zip(registerForRemoteNotifications())
.filter { identifiedEnvironment.identity.lastRegisteredDeviceToken != $1 } .filter { identification.identity.lastRegisteredDeviceToken != $1 }
.map { ($1, identifiedEnvironment.identity.pushSubscriptionAlerts) } .map { ($1, identification.identity.pushSubscriptionAlerts) }
.flatMap(identifiedEnvironment.identityService.createPushSubscription(deviceToken:alerts:)) .flatMap(identification.service.createPushSubscription(deviceToken:alerts:))
.sink { _ in } receiveValue: { _ in } .sink { _ in } receiveValue: { _ in }
.store(in: &cancellables) .store(in: &cancellables)
tabNavigationViewModel = TabNavigationViewModel(environment: identifiedEnvironment)
} }
func deleteIdentity(_ identity: Identity) { func deleteIdentity(_ identity: Identity) {

View File

@ -1,30 +0,0 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import ServiceLayer
public class SecondaryNavigationViewModel: ObservableObject {
@Published public private(set) var identity: Identity
private let environment: IdentifiedEnvironment
init(environment: IdentifiedEnvironment) {
self.environment = environment
identity = environment.identity
environment.$identity.dropFirst().assign(to: &$identity)
}
}
public extension SecondaryNavigationViewModel {
func identitiesViewModel() -> IdentitiesViewModel {
IdentitiesViewModel(environment: environment)
}
func listsViewModel() -> ListsViewModel {
ListsViewModel(environment: environment)
}
func preferencesViewModel() -> PreferencesViewModel {
PreferencesViewModel(environment: environment)
}
}

View File

@ -14,19 +14,19 @@ public class TabNavigationViewModel: ObservableObject {
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
public var selectedTab: Tab? = .timelines public var selectedTab: Tab? = .timelines
private let environment: IdentifiedEnvironment private let identification: Identification
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(environment: IdentifiedEnvironment) { public init(identification: Identification) {
self.environment = environment self.identification = identification
identity = environment.identity identity = identification.identity
environment.$identity.dropFirst().assign(to: &$identity) identification.$identity.dropFirst().assign(to: &$identity)
environment.identityService.recentIdentitiesObservation() identification.service.recentIdentitiesObservation()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$recentIdentities) .assign(to: &$recentIdentities)
environment.identityService.listsObservation() identification.service.listsObservation()
.map { Timeline.nonLists + $0 } .map { Timeline.nonLists + $0 }
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$timelinesAndLists) .assign(to: &$timelinesAndLists)
@ -54,42 +54,38 @@ public extension TabNavigationViewModel {
} }
func refreshIdentity() { func refreshIdentity() {
if environment.identityService.isAuthorized { if identification.service.isAuthorized {
environment.identityService.verifyCredentials() identification.service.verifyCredentials()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
environment.identityService.refreshLists() identification.service.refreshLists()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
environment.identityService.refreshFilters() identification.service.refreshFilters()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
if identity.preferences.useServerPostingReadingPreferences { if identity.preferences.useServerPostingReadingPreferences {
environment.identityService.refreshServerPreferences() identification.service.refreshServerPreferences()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }
} }
environment.identityService.refreshInstance() identification.service.refreshInstance()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }
func secondaryNavigationViewModel() -> SecondaryNavigationViewModel {
SecondaryNavigationViewModel(environment: environment)
}
func viewModel(timeline: Timeline) -> StatusListViewModel { func viewModel(timeline: Timeline) -> StatusListViewModel {
StatusListViewModel(statusListService: environment.identityService.service(timeline: timeline)) StatusListViewModel(statusListService: identification.service.service(timeline: timeline))
} }
} }

View File

@ -46,15 +46,8 @@ class AddIdentityViewModelTests: XCTestCase {
} }
func testDoesNotAlertCanceledLogin() throws { func testDoesNotAlertCanceledLogin() throws {
let environment = AppEnvironment( let allIdentitiesService = try AllIdentitiesService(
session: Session(configuration: .stubbing), environment: .mock(webAuthSessionType: CanceledLoginMockWebAuthSession.self))
webAuthSessionType: CanceledLoginMockWebAuthSession.self,
keychain: MockKeychain.self,
userDefaults: MockUserDefaults(),
userNotificationClient: .mock,
inMemoryContent: true,
identityFixture: nil)
let allIdentitiesService = try AllIdentitiesService(environment: environment)
let sut = AddIdentityViewModel(allIdentitiesService: allIdentitiesService) let sut = AddIdentityViewModel(allIdentitiesService: allIdentitiesService)
let recorder = sut.$alertItem.record() let recorder = sut.$alertItem.record()

View File

@ -14,7 +14,7 @@ class RootViewModelTests: XCTestCase {
let sut = try RootViewModel( let sut = try RootViewModel(
environment: .mock(), environment: .mock(),
registerForRemoteNotifications: { Empty().setFailureType(to: Error.self).eraseToAnyPublisher() }) registerForRemoteNotifications: { Empty().setFailureType(to: Error.self).eraseToAnyPublisher() })
let recorder = sut.$tabNavigationViewModel.record() let recorder = sut.$identification.record()
XCTAssertNil(try wait(for: recorder.next(), timeout: 1)) XCTAssertNil(try wait(for: recorder.next(), timeout: 1))

View File

@ -46,7 +46,7 @@ import PreviewViewModels
struct AddAccountView_Previews: PreviewProvider { struct AddAccountView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
AddIdentityView(viewModel: .mock()) AddIdentityView(viewModel: RootViewModel.preview.addIdentityViewModel())
} }
} }
#endif #endif

View File

@ -101,7 +101,7 @@ import PreviewViewModels
struct EditFilterView_Previews: PreviewProvider { struct EditFilterView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
EditFilterView(viewModel: .mock()) EditFilterView(viewModel: .init(filter: .new, identification: .preview))
} }
} }
#endif #endif

View File

@ -6,12 +6,13 @@ import ViewModels
struct FiltersView: View { struct FiltersView: View {
@StateObject var viewModel: FiltersViewModel @StateObject var viewModel: FiltersViewModel
@EnvironmentObject var identification: Identification
var body: some View { var body: some View {
Form { Form {
Section { Section {
NavigationLink(destination: EditFilterView( NavigationLink(destination: EditFilterView(
viewModel: viewModel.editFilterViewModel(filter: .new))) { viewModel: .init(filter: .new, identification: identification))) {
Label("add", systemImage: "plus.circle") Label("add", systemImage: "plus.circle")
} }
} }
@ -36,7 +37,7 @@ private extension FiltersView {
Section(header: Text(title)) { Section(header: Text(title)) {
ForEach(filters) { filter in ForEach(filters) { filter in
NavigationLink(destination: EditFilterView( NavigationLink(destination: EditFilterView(
viewModel: viewModel.editFilterViewModel(filter: filter))) { viewModel: .init(filter: filter, identification: identification))) {
HStack { HStack {
Text(filter.phrase) Text(filter.phrase)
Spacer() Spacer()
@ -60,7 +61,7 @@ import PreviewViewModels
struct FiltersView_Previews: PreviewProvider { struct FiltersView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
FiltersView(viewModel: .mock()) FiltersView(viewModel: .init(identification: .preview))
} }
} }
#endif #endif

View File

@ -72,8 +72,8 @@ import PreviewViewModels
struct IdentitiesView_Previews: PreviewProvider { struct IdentitiesView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
IdentitiesView(viewModel: .mock()) IdentitiesView(viewModel: .init(identification: .preview))
.environmentObject(RootViewModel.mock()) .environmentObject(RootViewModel.preview)
} }
} }
#endif #endif

View File

@ -60,8 +60,8 @@ import PreviewViewModels
struct ListsView_Previews: PreviewProvider { struct ListsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ListsView(viewModel: .mock()) ListsView(viewModel: .init(identification: .preview))
.environmentObject(TabNavigationViewModel.mock()) .environmentObject(TabNavigationViewModel(identification: .preview))
} }
} }
#endif #endif

View File

@ -29,7 +29,7 @@ import PreviewViewModels
struct NotificationTypesPreferencesView_Previews: PreviewProvider { struct NotificationTypesPreferencesView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NotificationTypesPreferencesView(viewModel: .mock()) NotificationTypesPreferencesView(viewModel: .init(identification: .preview))
} }
} }
#endif #endif

View File

@ -55,7 +55,7 @@ import PreviewViewModels
struct PostingReadingPreferencesViewView_Previews: PreviewProvider { struct PostingReadingPreferencesViewView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
PostingReadingPreferencesView(viewModel: .mock()) PostingReadingPreferencesView(viewModel: .init(identification: .preview))
} }
} }
#endif #endif

View File

@ -5,20 +5,21 @@ import ViewModels
struct PreferencesView: View { struct PreferencesView: View {
@StateObject var viewModel: PreferencesViewModel @StateObject var viewModel: PreferencesViewModel
@EnvironmentObject var identification: Identification
var body: some View { var body: some View {
Form { Form {
Section(header: Text(viewModel.handle)) { Section(header: Text(viewModel.handle)) {
NavigationLink("preferences.posting-reading", NavigationLink("preferences.posting-reading",
destination: PostingReadingPreferencesView( destination: PostingReadingPreferencesView(
viewModel: viewModel.postingReadingPreferencesViewModel())) viewModel: .init(identification: identification)))
NavigationLink("preferences.filters", NavigationLink("preferences.filters",
destination: FiltersView( destination: FiltersView(
viewModel: viewModel.filtersViewModel())) viewModel: .init(identification: identification)))
if viewModel.shouldShowNotificationTypePreferences { if viewModel.shouldShowNotificationTypePreferences {
NavigationLink("preferences.notification-types", NavigationLink("preferences.notification-types",
destination: NotificationTypesPreferencesView( destination: NotificationTypesPreferencesView(
viewModel: viewModel.notificationTypesPreferencesViewModel())) viewModel: .init(identification: identification)))
} }
} }
} }
@ -31,7 +32,7 @@ import PreviewViewModels
struct PreferencesView_Previews: PreviewProvider { struct PreferencesView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
PreferencesView(viewModel: .mock()) PreferencesView(viewModel: .init(identification: .preview))
} }
} }
#endif #endif

View File

@ -7,9 +7,11 @@ struct RootView: View {
@StateObject var viewModel: RootViewModel @StateObject var viewModel: RootViewModel
var body: some View { var body: some View {
if let tabNavigationViewModel = viewModel.tabNavigationViewModel { if let identification = viewModel.identification {
TabNavigationView(viewModel: tabNavigationViewModel) TabNavigationView()
.id(UUID()) .id(UUID())
.environmentObject(identification)
.environmentObject(TabNavigationViewModel(identification: identification))
.environmentObject(viewModel) .environmentObject(viewModel)
.transition(.opacity) .transition(.opacity)
} else { } else {
@ -21,11 +23,12 @@ struct RootView: View {
} }
#if DEBUG #if DEBUG
import Combine
import PreviewViewModels import PreviewViewModels
struct ContentView_Previews: PreviewProvider { struct ContentView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
RootView(viewModel: .mock()) RootView(viewModel: .preview)
} }
} }
#endif #endif

View File

@ -5,7 +5,8 @@ import SwiftUI
import ViewModels import ViewModels
struct SecondaryNavigationView: View { struct SecondaryNavigationView: View {
@StateObject var viewModel: SecondaryNavigationViewModel @EnvironmentObject var identification: Identification
@EnvironmentObject var tabNavigationViewModel: TabNavigationViewModel
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@Environment(\.displayScale) var displayScale: CGFloat @Environment(\.displayScale) var displayScale: CGFloat
@ -14,19 +15,19 @@ struct SecondaryNavigationView: View {
Form { Form {
Section { Section {
NavigationLink( NavigationLink(
destination: IdentitiesView(viewModel: viewModel.identitiesViewModel()), destination: IdentitiesView(viewModel: .init(identification: identification)),
label: { label: {
HStack { HStack {
KFImage(viewModel.identity.image, KFImage(tabNavigationViewModel.identity.image,
options: .downsampled(dimension: 50, scaleFactor: displayScale)) options: .downsampled(dimension: 50, scaleFactor: displayScale))
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let account = viewModel.identity.account { if let account = tabNavigationViewModel.identity.account {
CustomEmojiText( CustomEmojiText(
text: account.displayName, text: account.displayName,
emoji: account.emojis, emoji: account.emojis,
textStyle: .headline) textStyle: .headline)
} }
Text(viewModel.identity.handle) Text(tabNavigationViewModel.identity.handle)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
@ -40,7 +41,7 @@ struct SecondaryNavigationView: View {
}) })
} }
Section { Section {
NavigationLink(destination: ListsView(viewModel: viewModel.listsViewModel())) { NavigationLink(destination: ListsView(viewModel: .init(identification: identification))) {
Label("secondary-navigation.lists", systemImage: "scroll") Label("secondary-navigation.lists", systemImage: "scroll")
} }
} }
@ -48,7 +49,7 @@ struct SecondaryNavigationView: View {
NavigationLink( NavigationLink(
"secondary-navigation.preferences", "secondary-navigation.preferences",
destination: PreferencesView( destination: PreferencesView(
viewModel: viewModel.preferencesViewModel())) viewModel: .init(identification: identification)))
} }
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
@ -71,9 +72,9 @@ import PreviewViewModels
struct SecondaryNavigationView_Previews: PreviewProvider { struct SecondaryNavigationView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SecondaryNavigationView(viewModel: .mock()) SecondaryNavigationView()
.environmentObject(RootViewModel.mock()) .environmentObject(Identification.preview)
.environmentObject(TabNavigationViewModel.mock()) .environmentObject(TabNavigationViewModel(identification: .preview))
} }
} }
#endif #endif

View File

@ -20,7 +20,7 @@ import PreviewViewModels
struct StatusListView_Previews: PreviewProvider { struct StatusListView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
StatusListView(viewModel: .mock()) StatusListView(viewModel: TabNavigationViewModel(identification: .preview).viewModel(timeline: .home))
} }
} }
#endif #endif

View File

@ -6,7 +6,7 @@ import SwiftUI
import ViewModels import ViewModels
struct TabNavigationView: View { struct TabNavigationView: View {
@ObservedObject var viewModel: TabNavigationViewModel @EnvironmentObject var viewModel: TabNavigationViewModel
@EnvironmentObject var rootViewModel: RootViewModel @EnvironmentObject var rootViewModel: RootViewModel
@Environment(\.displayScale) var displayScale: CGFloat @Environment(\.displayScale) var displayScale: CGFloat
@ -25,7 +25,7 @@ struct TabNavigationView: View {
} }
} }
.sheet(isPresented: $viewModel.presentingSecondaryNavigation) { .sheet(isPresented: $viewModel.presentingSecondaryNavigation) {
SecondaryNavigationView(viewModel: viewModel.secondaryNavigationViewModel()) SecondaryNavigationView()
.environmentObject(viewModel) .environmentObject(viewModel)
} }
.alertItem($viewModel.alertItem) .alertItem($viewModel.alertItem)
@ -145,8 +145,10 @@ import PreviewViewModels
struct TabNavigation_Previews: PreviewProvider { struct TabNavigation_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
TabNavigationView(viewModel: .mock()) TabNavigationView()
.environmentObject(RootViewModel.mock()) .environmentObject(Identification.preview)
.environmentObject(TabNavigationViewModel(identification: .preview))
.environmentObject(RootViewModel.preview)
} }
} }
#endif #endif