Skip to content

Commit

Permalink
Standups -> SyncUps (#2524)
Browse files Browse the repository at this point in the history
* Standups -> SyncUps

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
  • Loading branch information
stephencelis authored Oct 17, 2023
1 parent bd6c1fc commit a611f14
Show file tree
Hide file tree
Showing 46 changed files with 406 additions and 407 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This directory holds many case studies and applications to demonstrate solving v
* **Speech Recognition**
<br> This application uses Apple's Speech framework to demonstrate how to wrap complex dependencies in the `Effect` type of the Composable Architecture. Doing a little bit of upfront work allows you to interact with the dependencies in a controlled, understandable way, and you can write tests on how the dependency interacts with your application logic.

* **Standups**
* **SyncUps**
<br> This application is a faithful reconstruction of one of Apple's more interesting sample projects, called [Scrumdinger][scrumdinger]. It deals with many forms of navigation (alerts, sheets, drill-downs) and many forms of side effects (data persistence, timers and speech recognizers).

* **Tic-Tac-Toe**
Expand All @@ -23,4 +23,4 @@ This directory holds many case studies and applications to demonstrate solving v
* **Voice Memos**
<br> A more complex demo that demonstrates how to work with many complex dependencies at once, and how to manage a complex state machine driven off of timers.

[scrumdinger]: https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger
[scrumdinger]: https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger
12 changes: 6 additions & 6 deletions Examples/Standups/README.md → Examples/SyncUps/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Standups
# SyncUps

This project demonstrates how to build a complex, real world application that deals with many forms
of navigation (_e.g._, sheets, drill-downs, alerts), many side effects (timers, speech recognizer,
Expand Down Expand Up @@ -27,14 +27,14 @@ But, the simplicity of Apple's Scrumdinger codebase is not a defect. In fact, it
Apple's sample code is viewed by hundreds of thousands of developers across the world, and so its
goal is to be as approachable as possible in order to teach the basics of SwiftUI. But, that doesn't mean there isn't room for improvement.

## Composable Standups
## Composable SyncUps

Our Standups application is a rebuild of Apple's Scrumdinger application, but with a focus on
Our SyncUps application is a rebuild of Apple's Scrumdinger application, but with a focus on
modern, best practices for SwiftUI development. We faithfully recreate the Scrumdinger, but with
some key additions:

1. Identifiers are made type safe using our [Tagged library][tagged-gh]. This prevents us from
writing nonsensical code, such as comparing a `Standup.ID` to a `Attendee.ID`.
writing nonsensical code, such as comparing a `SyncUp.ID` to a `Attendee.ID`.
2. Instead of using bare arrays in feature logic we use an "identified" array from our
[IdentifiedCollections][identified-collections-gh] library. This allows you to read and modify
elements of the collection via their ID rather than positional index, which can be error-prone
Expand All @@ -53,8 +53,8 @@ some key additions:
[Dependencies][dependencies-gh] library.
6. The project includes a full test suite. Since all of navigation is driven off of state, and
because we controlled all dependencies, we can write very comprehensive and nuanced tests. For
example, we can write a unit test that proves that when a standup meeting's timer runs out the
screen pops off the stack and a new transcript is added to the standup. Such a test would be
example, we can write a unit test that proves that when a sync-up meeting's timer runs out the
screen pops off the stack and a new transcript is added to the sync-up. Such a test would be
very difficult, if not impossible, without controlling dependencies.

[scrumdinger]: https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC808D6E29E9C3AC0072B4A9"
BuildableName = "Standups.app"
BlueprintName = "Standups"
ReferencedContainer = "container:Standups.xcodeproj">
BuildableName = "SyncUps.app"
BlueprintName = "SyncUps"
ReferencedContainer = "container:SyncUps.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
Expand All @@ -34,19 +34,19 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC808D7E29E9C3AD0072B4A9"
BuildableName = "StandupsTests.xctest"
BlueprintName = "StandupsTests"
ReferencedContainer = "container:Standups.xcodeproj">
BuildableName = "SyncUpsTests.xctest"
BlueprintName = "SyncUpsTests"
ReferencedContainer = "container:SyncUps.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC808D8829E9C3AD0072B4A9"
BuildableName = "StandupsUITests.xctest"
BlueprintName = "StandupsUITests"
ReferencedContainer = "container:Standups.xcodeproj">
BuildableName = "SyncUpsUITests.xctest"
BlueprintName = "SyncUpsUITests"
ReferencedContainer = "container:SyncUps.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
Expand All @@ -66,9 +66,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC808D6E29E9C3AC0072B4A9"
BuildableName = "Standups.app"
BlueprintName = "Standups"
ReferencedContainer = "container:Standups.xcodeproj">
BuildableName = "SyncUps.app"
BlueprintName = "SyncUps"
ReferencedContainer = "container:SyncUps.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
Expand All @@ -83,9 +83,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC808D6E29E9C3AC0072B4A9"
BuildableName = "Standups.app"
BlueprintName = "Standups"
ReferencedContainer = "container:Standups.xcodeproj">
BuildableName = "SyncUps.app"
BlueprintName = "SyncUps"
ReferencedContainer = "container:SyncUps.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import ComposableArchitecture
import SwiftUI

@main
struct StandupsApp: App {
struct SyncUpsApp: App {
var body: some Scene {
WindowGroup {
// NB: This conditional is here only to facilitate UI testing so that we can mock out certain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import SwiftUI
struct AppFeature: Reducer {
struct State: Equatable {
var path = StackState<Path.State>()
var standupsList = StandupsList.State()
var syncUpsList = SyncUpsList.State()
}

enum Action: Equatable {
case path(StackAction<Path.State, Path.Action>)
case standupsList(StandupsList.Action)
case syncUpsList(SyncUpsList.Action)
}

@Dependency(\.continuousClock) var clock
Expand All @@ -22,8 +22,8 @@ struct AppFeature: Reducer {
}

var body: some ReducerOf<Self> {
Scope(state: \.standupsList, action: /Action.standupsList) {
StandupsList()
Scope(state: \.syncUpsList, action: /Action.syncUpsList) {
SyncUpsList()
}
Reduce { state, action in
switch action {
Expand All @@ -32,16 +32,16 @@ struct AppFeature: Reducer {
else { return .none }

switch delegateAction {
case .deleteStandup:
state.standupsList.standups.remove(id: detailState.standup.id)
case .deleteSyncUp:
state.syncUpsList.syncUps.remove(id: detailState.syncUp.id)
return .none

case let .standupUpdated(standup):
state.standupsList.standups[id: standup.id] = standup
case let .syncUpUpdated(syncUp):
state.syncUpsList.syncUps[id: syncUp.id] = syncUp
return .none

case .startMeeting:
state.path.append(.record(RecordMeeting.State(standup: detailState.standup)))
state.path.append(.record(RecordMeeting.State(syncUp: detailState.syncUp)))
return .none
}

Expand All @@ -58,24 +58,24 @@ struct AppFeature: Reducer {
return .none
}

state.path[id: id, case: /Path.State.detail]?.standup.meetings.insert(
state.path[id: id, case: /Path.State.detail]?.syncUp.meetings.insert(
Meeting(
id: Meeting.ID(self.uuid()),
date: self.now,
transcript: transcript
),
at: 0
)
guard let standup = state.path[id: id, case: /Path.State.detail]?.standup
guard let syncUp = state.path[id: id, case: /Path.State.detail]?.syncUp
else { return .none }
state.standupsList.standups[id: standup.id] = standup
state.syncUpsList.syncUps[id: syncUp.id] = syncUp
return .none
}

case .path:
return .none

case .standupsList:
case .syncUpsList:
return .none
}
}
Expand All @@ -84,10 +84,10 @@ struct AppFeature: Reducer {
}

Reduce { state, action in
return .run { [standups = state.standupsList.standups] _ in
return .run { [syncUps = state.syncUpsList.syncUps] _ in
try await withTaskCancellation(id: CancelID.saveDebounce, cancelInFlight: true) {
try await self.clock.sleep(for: .seconds(1))
try await self.saveData(JSONEncoder().encode(standups), .standups)
try await self.saveData(JSONEncoder().encode(syncUps), .syncUps)
}
} catch: { _, _ in
}
Expand All @@ -96,19 +96,19 @@ struct AppFeature: Reducer {

struct Path: Reducer {
enum State: Equatable {
case detail(StandupDetail.State)
case meeting(Meeting, standup: Standup)
case detail(SyncUpDetail.State)
case meeting(Meeting, syncUp: SyncUp)
case record(RecordMeeting.State)
}

enum Action: Equatable {
case detail(StandupDetail.Action)
case detail(SyncUpDetail.Action)
case record(RecordMeeting.Action)
}

var body: some Reducer<State, Action> {
Scope(state: /State.detail, action: /Action.detail) {
StandupDetail()
SyncUpDetail()
}
Scope(state: /State.record, action: /Action.record) {
RecordMeeting()
Expand All @@ -122,19 +122,19 @@ struct AppView: View {

var body: some View {
NavigationStackStore(self.store.scope(state: \.path, action: { .path($0) })) {
StandupsListView(
store: self.store.scope(state: \.standupsList, action: { .standupsList($0) })
SyncUpsListView(
store: self.store.scope(state: \.syncUpsList, action: { .syncUpsList($0) })
)
} destination: {
switch $0 {
case .detail:
CaseLet(
/AppFeature.Path.State.detail,
action: AppFeature.Path.Action.detail,
then: StandupDetailView.init(store:)
then: SyncUpDetailView.init(store:)
)
case let .meeting(meeting, standup: standup):
MeetingView(meeting: meeting, standup: standup)
case let .meeting(meeting, syncUp: syncUp):
MeetingView(meeting: meeting, syncUp: syncUp)
case .record:
CaseLet(
/AppFeature.Path.State.record,
Expand All @@ -147,5 +147,5 @@ struct AppView: View {
}

extension URL {
static let standups = Self.documentsDirectory.appending(component: "standups.json")
static let syncUps = Self.documentsDirectory.appending(component: "sync-ups.json")
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftUI

struct MeetingView: View {
let meeting: Meeting
let standup: Standup
let syncUp: SyncUp

var body: some View {
ScrollView {
Expand All @@ -12,7 +12,7 @@ struct MeetingView: View {
.padding(.bottom)
Text("Attendees")
.font(.headline)
ForEach(self.standup.attendees) { attendee in
ForEach(self.syncUp.attendees) { attendee in
Text(attendee.name)
}
Text("Transcript")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import IdentifiedCollections
import SwiftUI
import Tagged

struct Standup: Equatable, Identifiable, Codable {
struct SyncUp: Equatable, Identifiable, Codable {
let id: Tagged<Self, UUID>
var attendees: IdentifiedArrayOf<Attendee> = []
var duration: Duration = .seconds(60 * 5)
Expand Down Expand Up @@ -61,9 +61,9 @@ enum Theme: String, CaseIterable, Equatable, Identifiable, Codable {
var name: String { self.rawValue.capitalized }
}

extension Standup {
extension SyncUp {
static let mock = Self(
id: Standup.ID(),
id: SyncUp.ID(),
attendees: [
Attendee(id: Attendee.ID(), name: "Blob"),
Attendee(id: Attendee.ID(), name: "Blob Jr"),
Expand Down Expand Up @@ -92,7 +92,7 @@ extension Standup {
)

static let engineeringMock = Self(
id: Standup.ID(),
id: SyncUp.ID(),
attendees: [
Attendee(id: Attendee.ID(), name: "Blob"),
Attendee(id: Attendee.ID(), name: "Blob Jr"),
Expand All @@ -103,7 +103,7 @@ extension Standup {
)

static let designMock = Self(
id: Standup.ID(),
id: SyncUp.ID(),
attendees: [
Attendee(id: Attendee.ID(), name: "Blob Sr"),
Attendee(id: Attendee.ID(), name: "Blob Jr"),
Expand Down
Loading

0 comments on commit a611f14

Please sign in to comment.