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
Branches
Tags
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