Skip to content
Snippets Groups Projects
Commit 4197d79d authored by Dariusz Rybicki's avatar Dariusz Rybicki
Browse files

Move app launch logic to AppFeature

parent f1f148bb
No related branches found
No related tags found
2 merge requests!102Release 1.0.0,!57Update messenger example
......@@ -69,8 +69,6 @@ let package = Package(
dependencies: [
.target(name: "AppCore"),
.target(name: "HomeFeature"),
.target(name: "LaunchFeature"),
.target(name: "RegisterFeature"),
.target(name: "RestoreFeature"),
.target(name: "WelcomeFeature"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
......
import AppCore
import Foundation
import HomeFeature
import LaunchFeature
import RegisterFeature
import RestoreFeature
import WelcomeFeature
......@@ -17,8 +16,6 @@ extension AppEnvironment {
let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler()
return AppEnvironment(
launch: {
LaunchEnvironment(
dbManager: dbManager,
messenger: messenger,
mainQueue: mainQueue,
......@@ -33,15 +30,6 @@ extension AppEnvironment {
restore: {
RestoreEnvironment()
},
register: {
RegisterEnvironment(
messenger: messenger,
mainQueue: mainQueue,
bgQueue: bgQueue
)
}
)
},
home: {
HomeEnvironment(
messenger: messenger,
......
import AppCore
import Combine
import ComposableArchitecture
import ComposablePresentation
import Foundation
import HomeFeature
import LaunchFeature
import RestoreFeature
import WelcomeFeature
import XXMessengerClient
struct AppState: Equatable {
enum Screen: Equatable {
case launch(LaunchState)
case loading
case welcome(WelcomeState)
case restore(RestoreState)
case home(HomeState)
case failure(String)
}
var screen: Screen = .launch(LaunchState())
@BindableState var screen: Screen = .loading
}
extension AppState.Screen {
var asLaunch: LaunchState? {
get { (/AppState.Screen.launch).extract(from: self) }
set { if let newValue = newValue { self = .launch(newValue) } }
var asWelcome: WelcomeState? {
get { (/AppState.Screen.welcome).extract(from: self) }
set { if let newValue = newValue { self = .welcome(newValue) } }
}
var asRestore: RestoreState? {
get { (/AppState.Screen.restore).extract(from: self) }
set { if let state = newValue { self = .restore(state) } }
}
var asHome: HomeState? {
get { (/AppState.Screen.home).extract(from: self) }
......@@ -23,19 +35,32 @@ extension AppState.Screen {
}
}
enum AppAction: Equatable {
enum AppAction: Equatable, BindableAction {
case start
case binding(BindingAction<AppState>)
case welcome(WelcomeAction)
case restore(RestoreAction)
case home(HomeAction)
case launch(LaunchAction)
}
struct AppEnvironment {
var launch: () -> LaunchEnvironment
var dbManager: DBManager
var messenger: Messenger
var mainQueue: AnySchedulerOf<DispatchQueue>
var bgQueue: AnySchedulerOf<DispatchQueue>
var welcome: () -> WelcomeEnvironment
var restore: () -> RestoreEnvironment
var home: () -> HomeEnvironment
}
extension AppEnvironment {
static let unimplemented = AppEnvironment(
launch: { .unimplemented },
dbManager: .unimplemented,
messenger: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented,
welcome: { .unimplemented },
restore: { .unimplemented },
home: { .unimplemented }
)
}
......@@ -43,20 +68,60 @@ extension AppEnvironment {
let appReducer = Reducer<AppState, AppAction, AppEnvironment>
{ state, action, env in
switch action {
case .launch(.finished):
state.screen = .home(HomeState())
case .start, .welcome(.finished), .restore(.finished):
state.screen = .loading
return .run { subscriber in
do {
if env.dbManager.hasDB() == false {
try env.dbManager.makeDB()
}
if env.messenger.isLoaded() == false {
if env.messenger.isCreated() == false {
subscriber.send(.set(\.$screen, .welcome(WelcomeState())))
subscriber.send(completion: .finished)
return AnyCancellable {}
}
try env.messenger.load()
}
subscriber.send(.set(\.$screen, .home(HomeState())))
} catch {
subscriber.send(.set(\.$screen, .failure(error.localizedDescription)))
}
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .welcome(.restoreTapped):
state.screen = .restore(RestoreState())
return .none
case .welcome(.failed(let failure)):
state.screen = .failure(failure)
return .none
case .launch(_), .home(_):
case .binding(_), .welcome(_), .restore(_), .home(_):
return .none
}
}
.binding()
.presenting(
welcomeReducer,
state: .keyPath(\.screen.asWelcome),
id: .notNil(),
action: /AppAction.welcome,
environment: { $0.welcome() }
)
.presenting(
launchReducer,
state: .keyPath(\.screen.asLaunch),
restoreReducer,
state: .keyPath(\.screen.asRestore),
id: .notNil(),
action: /AppAction.launch,
environment: { $0.launch() }
action: /AppAction.restore,
environment: { $0.restore() }
)
.presenting(
homeReducer,
......
import ComposableArchitecture
import SwiftUI
import HomeFeature
import LaunchFeature
import RestoreFeature
import SwiftUI
import WelcomeFeature
struct AppView: View {
let store: Store<AppState, AppAction>
enum ViewState: Equatable {
case launch
case loading
case welcome
case restore
case home
case failure(String)
init(_ state: AppState) {
switch state.screen {
case .launch(_): self = .launch
case .loading: self = .loading
case .welcome(_): self = .welcome
case .restore(_): self = .restore
case .home(_): self = .home
case .failure(let failure): self = .failure(failure)
}
}
}
......@@ -21,20 +28,53 @@ struct AppView: View {
var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
ZStack {
SwitchStore(store.scope(state: \.screen)) {
CaseLet(
state: /AppState.Screen.launch,
action: AppAction.launch,
then: { store in
LaunchView(store: store)
switch viewStore.state {
case .loading:
ProgressView {
Text("Loading")
}
.controlSize(.large)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(.opacity)
case .welcome:
IfLetStore(
store.scope(
state: { (/AppState.Screen.welcome).extract(from: $0.screen) },
action: AppAction.welcome
),
then: { store in
WelcomeView(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(.asymmetric(
insertion: .move(edge: .trailing),
removal: .opacity
))
}
)
case .restore:
IfLetStore(
store.scope(
state: { (/AppState.Screen.restore).extract(from: $0.screen) },
action: AppAction.restore
),
then: { store in
RestoreView(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(.asymmetric(
insertion: .move(edge: .trailing),
removal: .opacity
))
}
)
CaseLet(
state: /AppState.Screen.home,
action: AppAction.home,
case .home:
IfLetStore(
store.scope(
state: { (/AppState.Screen.home).extract(from: $0.screen) },
action: AppAction.home
),
then: { store in
HomeView(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity)
......@@ -44,9 +84,40 @@ struct AppView: View {
))
}
)
case .failure(let failure):
NavigationView {
VStack(spacing: 0) {
ScrollView {
Text(failure)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
Divider()
Button {
viewStore.send(.start)
} label: {
Text("Retry")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.padding()
}
.navigationTitle("Error")
}
.navigationViewStyle(.stack)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(.asymmetric(
insertion: .move(edge: .trailing),
removal: .opacity
))
}
}
.animation(.default, value: viewStore.state)
.task { viewStore.send(.start) }
}
}
}
......
import ComposableArchitecture
import HomeFeature
import RestoreFeature
import WelcomeFeature
import XCTest
@testable import AppFeature
@MainActor
final class AppFeatureTests: XCTestCase {
func testLaunchFinished() async throws {
func testStartWithoutMessengerCreated() {
let store = TestStore(
initialState: AppState(),
reducer: appReducer,
environment: .unimplemented
)
await store.send(.launch(.finished)) {
let mainQueue = DispatchQueue.test
let bgQueue = DispatchQueue.test
var didMakeDB = 0
store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
store.environment.dbManager.hasDB.run = { false }
store.environment.dbManager.makeDB.run = { didMakeDB += 1 }
store.environment.messenger.isLoaded.run = { false }
store.environment.messenger.isCreated.run = { false }
store.send(.start)
bgQueue.advance()
XCTAssertNoDifference(didMakeDB, 1)
mainQueue.advance()
store.receive(.set(\.$screen, .welcome(WelcomeState()))) {
$0.screen = .welcome(WelcomeState())
}
}
func testStartWithMessengerCreated() {
let store = TestStore(
initialState: AppState(),
reducer: appReducer,
environment: .unimplemented
)
let mainQueue = DispatchQueue.test
let bgQueue = DispatchQueue.test
var didMakeDB = 0
var messengerDidLoad = 0
store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
store.environment.dbManager.hasDB.run = { false }
store.environment.dbManager.makeDB.run = { didMakeDB += 1 }
store.environment.messenger.isLoaded.run = { false }
store.environment.messenger.isCreated.run = { true }
store.environment.messenger.load.run = { messengerDidLoad += 1 }
store.send(.start)
bgQueue.advance()
XCTAssertNoDifference(didMakeDB, 1)
XCTAssertNoDifference(messengerDidLoad, 1)
mainQueue.advance()
store.receive(.set(\.$screen, .home(HomeState()))) {
$0.screen = .home(HomeState())
}
}
func testWelcomeFinished() {
let store = TestStore(
initialState: AppState(
screen: .welcome(WelcomeState())
),
reducer: appReducer,
environment: .unimplemented
)
let mainQueue = DispatchQueue.test
let bgQueue = DispatchQueue.test
var messengerDidLoad = 0
store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
store.environment.dbManager.hasDB.run = { true }
store.environment.messenger.isLoaded.run = { false }
store.environment.messenger.isCreated.run = { true }
store.environment.messenger.load.run = { messengerDidLoad += 1 }
store.send(.welcome(.finished)) {
$0.screen = .loading
}
bgQueue.advance()
XCTAssertNoDifference(messengerDidLoad, 1)
mainQueue.advance()
store.receive(.set(\.$screen, .home(HomeState()))) {
$0.screen = .home(HomeState())
}
}
func testRestoreFinished() {
let store = TestStore(
initialState: AppState(
screen: .restore(RestoreState())
),
reducer: appReducer,
environment: .unimplemented
)
let mainQueue = DispatchQueue.test
let bgQueue = DispatchQueue.test
var messengerDidLoad = 0
store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
store.environment.dbManager.hasDB.run = { true }
store.environment.messenger.isLoaded.run = { false }
store.environment.messenger.isCreated.run = { true }
store.environment.messenger.load.run = { messengerDidLoad += 1 }
store.send(.restore(.finished)) {
$0.screen = .loading
}
bgQueue.advance()
XCTAssertNoDifference(messengerDidLoad, 1)
mainQueue.advance()
store.receive(.set(\.$screen, .home(HomeState()))) {
$0.screen = .home(HomeState())
}
}
func testWelcomeRestoreTapped() {
let store = TestStore(
initialState: AppState(
screen: .welcome(WelcomeState())
),
reducer: appReducer,
environment: .unimplemented
)
store.send(.welcome(.restoreTapped)) {
$0.screen = .restore(RestoreState())
}
}
func testWelcomeFailed() {
let store = TestStore(
initialState: AppState(
screen: .welcome(WelcomeState())
),
reducer: appReducer,
environment: .unimplemented
)
let failure = "Something went wrong"
store.send(.welcome(.failed(failure))) {
$0.screen = .failure(failure)
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment