Skip to content

Commit

Permalink
introduce AppFlow to support different application flows
Browse files Browse the repository at this point in the history
  • Loading branch information
mormaer committed Sep 10, 2023
1 parent 69cbd0c commit 67b56da
Show file tree
Hide file tree
Showing 24 changed files with 268 additions and 215 deletions.
12 changes: 8 additions & 4 deletions Mlem.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
500C168E2A66FAAB006F243B /* HapticManager+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500C168D2A66FAAB006F243B /* HapticManager+Dependency.swift */; };
5016A2B12A67EB8600B257E8 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5016A2B02A67EB8600B257E8 /* UIViewController.swift */; };
5016A2B32A67EC0700B257E8 /* NotificationDisplayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5016A2B22A67EC0700B257E8 /* NotificationDisplayer.swift */; };
503422562AAB784000EFE88D /* Environment+AppFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503422552AAB784000EFE88D /* Environment+AppFlow.swift */; };
503422582AAB798600EFE88D /* AppFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503422572AAB798600EFE88D /* AppFlow.swift */; };
503A5D752A78EF3C00488C38 /* Encodable+Export.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503A5D742A78EF3C00488C38 /* Encodable+Export.swift */; };
503BA26F2A2C94540052516C /* URL+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503BA26E2A2C94540052516C /* URL+Identifiable.swift */; };
504106CD2A744D7F000AAEF8 /* CommentRepository+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504106CC2A744D7F000AAEF8 /* CommentRepository+Dependency.swift */; };
Expand Down Expand Up @@ -385,7 +387,6 @@
CDE6A8162A490AE00062D161 /* Inbox Message View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A8152A490AE00062D161 /* Inbox Message View.swift */; };
CDE6A8182A490AF20062D161 /* Inbox Mention View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A8172A490AF20062D161 /* Inbox Mention View.swift */; };
CDE6A81A2A490B970062D161 /* Inbox Reply View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A8192A490B970062D161 /* Inbox Reply View.swift */; };
CDE8F2392A68DA7D00E0AE68 /* Environment - Force Onboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE8F2382A68DA7D00E0AE68 /* Environment - Force Onboard.swift */; };
CDE9CE4C2A7B0831002B97DD /* Gentle Info.ahap in Resources */ = {isa = PBXBuildFile; fileRef = CDE9CE4B2A7B0831002B97DD /* Gentle Info.ahap */; };
CDE9CE4F2A7B0B1B002B97DD /* Haptic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE9CE4E2A7B0B1B002B97DD /* Haptic.swift */; };
CDE9CE512A7B0C66002B97DD /* Firmer Info.ahap in Resources */ = {isa = PBXBuildFile; fileRef = CDE9CE502A7B0C66002B97DD /* Firmer Info.ahap */; };
Expand Down Expand Up @@ -469,6 +470,8 @@
500C168D2A66FAAB006F243B /* HapticManager+Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HapticManager+Dependency.swift"; sourceTree = "<group>"; };
5016A2B02A67EB8600B257E8 /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
5016A2B22A67EC0700B257E8 /* NotificationDisplayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationDisplayer.swift; sourceTree = "<group>"; };
503422552AAB784000EFE88D /* Environment+AppFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+AppFlow.swift"; sourceTree = "<group>"; };
503422572AAB798600EFE88D /* AppFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlow.swift; sourceTree = "<group>"; };
503A5D742A78EF3C00488C38 /* Encodable+Export.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+Export.swift"; sourceTree = "<group>"; };
503BA26E2A2C94540052516C /* URL+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Identifiable.swift"; sourceTree = "<group>"; };
504106CC2A744D7F000AAEF8 /* CommentRepository+Dependency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CommentRepository+Dependency.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -818,7 +821,6 @@
CDE6A8152A490AE00062D161 /* Inbox Message View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Inbox Message View.swift"; sourceTree = "<group>"; };
CDE6A8172A490AF20062D161 /* Inbox Mention View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Inbox Mention View.swift"; sourceTree = "<group>"; };
CDE6A8192A490B970062D161 /* Inbox Reply View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Inbox Reply View.swift"; sourceTree = "<group>"; };
CDE8F2382A68DA7D00E0AE68 /* Environment - Force Onboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment - Force Onboard.swift"; sourceTree = "<group>"; };
CDE9CE4B2A7B0831002B97DD /* Gentle Info.ahap */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Gentle Info.ahap"; sourceTree = "<group>"; };
CDE9CE4E2A7B0B1B002B97DD /* Haptic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Haptic.swift; sourceTree = "<group>"; };
CDE9CE502A7B0C66002B97DD /* Firmer Info.ahap */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Firmer Info.ahap"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1285,7 +1287,6 @@
E4DDB4312A81819300B3A7E0 /* Double.swift */,
503A5D742A78EF3C00488C38 /* Encodable+Export.swift */,
B1955A202A6145C00056CF99 /* Environment - EasterFlagSetter.swift */,
CDE8F2382A68DA7D00E0AE68 /* Environment - Force Onboard.swift */,
507573902A5AD53C00AA7ABD /* Error+Equatable.swift */,
6D8601ED2A43C0B1002A56FC /* Image.swift */,
6DA61F842A568F99001EA633 /* Int.swift */,
Expand All @@ -1302,6 +1303,7 @@
503BA26E2A2C94540052516C /* URL+Identifiable.swift */,
63F0C7BA2A058CB700A18C5D /* URLSessionWebSocketTask - Send Ping.swift */,
50CC4A712A9CB07F0074C845 /* TimeInterval+Period.swift */,
503422552AAB784000EFE88D /* Environment+AppFlow.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -1417,6 +1419,7 @@
6363D5C427EE196700E34822 /* MlemApp.swift */,
B157E0C32A507B8000B02C8B /* Window.swift */,
6363D5C627EE196700E34822 /* ContentView.swift */,
503422572AAB798600EFE88D /* AppFlow.swift */,
6386E02B2A03D1EC006B3C1D /* App State.swift */,
63DF71F02A02999C002AC14E /* App Constants.swift */,
B1B78D632A51D53900F72485 /* AppDelegate.swift */,
Expand Down Expand Up @@ -2486,7 +2489,6 @@
637218752A3A2AAD008C4816 /* GetCommunity.swift in Sources */,
CDF1EF142A6B6D6E003594B6 /* Feed View Logic.swift in Sources */,
6DFF50432A48DED3001E648D /* Inbox View.swift in Sources */,
CDE8F2392A68DA7D00E0AE68 /* Environment - Force Onboard.swift in Sources */,
CDF8426F2A4A385A00723DA0 /* Inbox Item Type.swift in Sources */,
CD1446232A5B336900610EF1 /* LicensesView.swift in Sources */,
CDDCF6432A66343D003DA3AC /* FancyTabBar.swift in Sources */,
Expand All @@ -2499,6 +2501,7 @@
6372186F2A3A2AAD008C4816 /* SearchRequest.swift in Sources */,
CD05E7812A4F7A4B0081D102 /* Inbox Tracker.swift in Sources */,
50811B362A920519006BA3F2 /* APISite+Mock.swift in Sources */,
503422562AAB784000EFE88D /* Environment+AppFlow.swift in Sources */,
CDDCF6512A677E1B003DA3AC /* FancyTabItemPreferenceKeys.swift in Sources */,
CDEBC32A2A9A580B00518D9D /* Post Model.swift in Sources */,
CDC6A8CA2A6F1C8D00CC11AC /* AssociatedIconProtocol.swift in Sources */,
Expand All @@ -2523,6 +2526,7 @@
6317ABCB2A37292700603D76 /* FeedType.swift in Sources */,
CDC65D8F2A86B6DD007205E5 /* DeleteUser.swift in Sources */,
CD6483382A3A0F2200EE6CA3 /* NSFW Tag.swift in Sources */,
503422582AAB798600EFE88D /* AppFlow.swift in Sources */,
637218642A3A2AAD008C4816 /* GetPost.swift in Sources */,
63E5D3942A13CF3600EC1FBD /* Favorite Community.swift in Sources */,
CD4E98A12A69BE980026C4D9 /* AlternativeIconCell.swift in Sources */,
Expand Down
17 changes: 13 additions & 4 deletions Mlem/API/APIClient/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,19 @@ class APIClient {

// MARK: - Public methods

/// Configures the clients session based on the passed in account
/// - Parameter account: a `SavedAccount` to use when configuring the clients session
func configure(for account: SavedAccount) {
session = .authenticated(account.instanceLink, account.accessToken)
/// Configures the clients session based on the passed in flow
/// - Parameter flow: The application flow which the client should be configured for
func configure(for flow: AppFlow) {
switch flow {
case let .account(account):
session = .authenticated(account.instanceLink, account.accessToken)
case .onboarding:
// no calls to our `APIClient` should be made during onboarding
// excluding a _login_ call which requires an explicit session to be provided
// setting to `.undefined` here ensures that errors will be throw should a call
// be attempted
session = .undefined
}
}

@discardableResult
Expand Down
55 changes: 22 additions & 33 deletions Mlem/App State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,36 @@ class AppState: ObservableObject {
@Dependency(\.apiClient) var apiClient

@AppStorage("defaultAccountId") var defaultAccountId: Int?
@Binding private var selectedAccount: SavedAccount?
@Published private(set) var currentActiveAccount: SavedAccount
@Published private(set) var currentNickname: String

/// Initialises our app state
/// - Parameters:
/// - defaultAccount: The account the application should start with
/// - selectedAccount: A `Binding` to the selected account at the `Window` level
init(defaultAccount: SavedAccount, selectedAccount: Binding<SavedAccount?>) {
_selectedAccount = selectedAccount
self.currentActiveAccount = defaultAccount
self.currentNickname = defaultAccount.nickname
self.defaultAccountId = currentActiveAccount.id
accountUpdated()
}
@Published private(set) var currentActiveAccount: SavedAccount?
@Published private(set) var currentNickname: String?

/// A method to set the current active account
/// - Important: If you wish to _clear_ the current active account please use the `\.setAppFlow` method available via the environment to reset to our `.onboarding` flow
/// - Parameter account: The `SavedAccount` which should become the active account
func setActiveAccount(_ account: SavedAccount) {
// update our stored token and set the account...
AppConstants.keychain["\(account.id)_accessToken"] = account.accessToken
// we configure the client here to ensure any updated session tokens are updated
apiClient.configure(for: .account(account))
currentActiveAccount = account
defaultAccountId = currentActiveAccount.id

// if the account we just set is not the existing one from the session
// then the user is switching accounts, so we pass the value up to the
// `Window` layer which will re-create our `ContentView` and the new
// account will restart on the feed page with a clean slate
if account.id != selectedAccount?.id {
selectedAccount = account
return
}

accountUpdated()
currentNickname = account.nickname
defaultAccountId = account.id
}

/// Update the nickname. This is needed to quickly propagate changes from settings over to the tab bar, since nickname doesn't affect account identity and so changing it doesn't always prompt redraws
func changeDisplayedNickname(to nickname: String) {
currentNickname = nickname
/// A method to clear the currentlly active account
/// - Important: It is unlikely you will want to call this method directly but instead use the `\.setAppFlow` method available via the environment
func clearActiveAccount() {
currentActiveAccount = nil
currentNickname = nil
}

private func accountUpdated() {
// ensure our client session is updated
apiClient.configure(for: currentActiveAccount)
func isCurrentAccountId(_ id: Int) -> Bool {
guard let currentActiveAccount else { return false }
// TODO: we likely need to improve this check as comparing just the id might not be enough (same id, different instances)
// I'm going to leave this for now as if we wanted to move to using a value like `.actorId` then we'll need to
// to start storing it in the `SavedAccount` object first etc, which is getting well outside the scope of this PR...
// although the _check_ has moved in this PR, it's performing the same check that was being done elsewhere so there
// should be no regression introduced by only checking the `.id`
return currentActiveAccount.id == id
}
}
3 changes: 3 additions & 0 deletions Mlem/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import Foundation
import SwiftUI
import UIKit

// TODO: we need to do a bit of work to ensure we also switch tab when responding to these
// as currently it launches you into the app, but if the app was already running you're left
// on the tab/screen you were on - despite the shortcuts being design to take you to the feeds
var shortcutItemToProcess: UIApplicationShortcutItem?

class AppDelegate: UIResponder, UIApplicationDelegate, UIWindowSceneDelegate {
Expand Down
17 changes: 17 additions & 0 deletions Mlem/AppFlow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// AppFlow.swift
// Mlem
//
// Created by mormaer on 08/09/2023.
//
//

import Foundation

/// An enumeration that describes the types of flow that are supported by the application
enum AppFlow: Equatable {
/// The onboarding flow
case onboarding
/// An 'signed in'' session with the users `SavedAccount` as an associated value
case account(SavedAccount)
}
51 changes: 29 additions & 22 deletions Mlem/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,33 @@ struct ContentView: View {
activeSymbolName: "scroll.fill"
)
}
InboxView()
.fancyTabItem(tag: TabSelection.inbox) {
FancyTabBarLabel(
tag: TabSelection.inbox,
symbolName: "mail.stack",
activeSymbolName: "mail.stack.fill",
badgeCount: showInboxUnreadBadge ? unreadTracker.total : 0
)
}

ProfileView(userID: appState.currentActiveAccount.id)
.fancyTabItem(tag: TabSelection.profile) {
FancyTabBarLabel(
tag: TabSelection.profile,
customText: computeUsername(account: appState.currentActiveAccount),
symbolName: "person.circle",
activeSymbolName: "person.circle.fill"
)
.simultaneousGesture(accountSwitchLongPress)
}
// wrapping these two behind a check for an active user, as of now we'll always have one
// but when guest mode arrives we'll either omit these entirely, or replace them with a
// guest mode specific tab for sign in / change instance screen.
if let account = appState.currentActiveAccount {
InboxView()
.fancyTabItem(tag: TabSelection.inbox) {
FancyTabBarLabel(
tag: TabSelection.inbox,
symbolName: "mail.stack",
activeSymbolName: "mail.stack.fill",
badgeCount: showInboxUnreadBadge ? unreadTracker.total : 0
)
}

ProfileView(userID: account.id)
.fancyTabItem(tag: TabSelection.profile) {
FancyTabBarLabel(
tag: TabSelection.profile,
customText: computeUsername(account: account),
symbolName: "person.circle",
activeSymbolName: "person.circle.fill"
)
.simultaneousGesture(accountSwitchLongPress)
}
}

SearchView()
.fancyTabItem(tag: TabSelection.search) {
FancyTabBarLabel(
Expand All @@ -89,8 +96,8 @@ struct ContentView: View {
accountChanged()
}
.onReceive(errorHandler.$sessionExpired) { expired in
if expired {
NotificationDisplayer.presentTokenRefreshFlow(for: appState.currentActiveAccount) { updatedAccount in
if expired, let account = appState.currentActiveAccount {
NotificationDisplayer.presentTokenRefreshFlow(for: account) { updatedAccount in
appState.setActiveAccount(updatedAccount)
}
}
Expand Down Expand Up @@ -149,7 +156,7 @@ struct ContentView: View {
switch profileTabLabel {
case .username: return account.username
case .instance: return account.hostName ?? account.username
case .nickname: return appState.currentNickname
case .nickname: return appState.currentNickname ?? account.username
case .anonymous: return "Profile"
}
}
Expand Down
20 changes: 0 additions & 20 deletions Mlem/Extensions/Environment - Force Onboard.swift

This file was deleted.

20 changes: 20 additions & 0 deletions Mlem/Extensions/Environment+AppFlow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Environment+AppFlow.swift
// Mlem
//
// Created by mormaer on 08/09/2023.
//
//

import SwiftUI

private struct AppFlowSetter: EnvironmentKey {
static let defaultValue: (AppFlow) -> Void = { _ in }
}

extension EnvironmentValues {
var setAppFlow: (AppFlow) -> Void {
get { self[AppFlowSetter.self] }
set { self[AppFlowSetter.self] = newValue }
}
}
14 changes: 11 additions & 3 deletions Mlem/MlemApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import XCTestDynamicOverlay

@main
struct MlemApp: App {

@Dependency(\.accountsTracker) var accountsTracker

@AppStorage("lightOrDarkMode") var lightOrDarkMode: UIUserInterfaceStyle = .unspecified
Expand All @@ -24,7 +23,7 @@ struct MlemApp: App {
var body: some Scene {
WindowGroup {
if !_XCTIsTesting {
Window(selectedAccount: accountsTracker.defaultAccount)
Window(flow: initialFlow)
.onAppear {
var imageConfig = ImagePipeline.Configuration.withDataCache(name: "main", sizeLimit: AppConstants.cacheSize)
imageConfig.dataLoadingQueue = OperationQueue(maxConcurrentCount: 8)
Expand Down Expand Up @@ -73,7 +72,7 @@ struct MlemApp: App {
}
}

func setupAppShortcuts() {
private func setupAppShortcuts() {
guard accountsTracker.savedAccounts.first != nil else { return }

// Subscribed Feed
Expand Down Expand Up @@ -112,4 +111,13 @@ struct MlemApp: App {
allFeedItem
]
}

/// A variable describing the initial flow the application should run after start-up
private var initialFlow: AppFlow {
guard let account = accountsTracker.defaultAccount else {
return .onboarding
}

return .account(account)
}
}
Loading

0 comments on commit 67b56da

Please sign in to comment.