Skip to content

Commit

Permalink
0.25.0
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed Mar 14, 2024
1 parent 99d4eb0 commit 0d19a27
Show file tree
Hide file tree
Showing 18 changed files with 579 additions and 560 deletions.
2 changes: 1 addition & 1 deletion Examples/SyncUps/SyncUps.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@
repositoryURL = "https://github.com/dankinsoid/VDFlow";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.6.0;
minimumVersion = 4.9.0;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/dankinsoid/VDFlow",
"state" : {
"revision" : "243bae56ccb137e48d94034fa995ae7f42e8e095",
"version" : "4.6.0"
"revision" : "8a7dbab4e15ae68137eafa618279bc2c57e2d6ff",
"version" : "4.9.0"
}
}
],
Expand Down
Binary file not shown.
17 changes: 10 additions & 7 deletions Examples/SyncUps/SyncUps/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import SwiftUI
@main
struct SyncUpsApp: App {

let store = Store(AppFeature()).transformDI {
if ProcessInfo.processInfo.environment["UITesting"] == "true" {
$0.dataManager = .mock()
}
}
.saveOnChange
let store = Store(AppFeature())

var body: some Scene {
WindowGroup {
Expand All @@ -21,7 +16,15 @@ struct SyncUpsApp: App {
// NB: Don't run application when testing so that it doesn't interfere with tests.
EmptyView()
} else {
AppView(store: store)
AppView(
store: store
.transformDI {
if ProcessInfo.processInfo.environment["UITesting"] == "true" {
$0.dataManager = .mock()
}
}
.saveOnChange
)
}
}
}
Expand Down
155 changes: 86 additions & 69 deletions Examples/SyncUps/SyncUps/AppFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,107 +4,124 @@ import VDFlow

struct AppFeature: Equatable {

var path = Path(.detail)
var path = Path(.list)
var syncUpsList = SyncUpsList()

@Steps
struct Path: Equatable {
var detail: SyncUpDetail?
var list
var detail = SyncUpDetail(syncUp: .engineeringMock)
var meeting = MeetingSyncUp()
var record: RecordMeeting?
var record: RecordMeeting = .mock

struct MeetingSyncUp: Equatable {
var meeting: Meeting?
var syncUp: SyncUp?
var meeting: Meeting = .mock
var syncUp: SyncUp = .engineeringMock
}
}
}

@Actions
extension Store<AppFeature> {
extension Store<AppFeature>: SyncUpDetailDelegate {

func setPathDetail(id: String, detail: SyncUpDetail) {
switch detail {
case let .delegate(delegateAction):
guard case let .some(.detail(detailState)) = state.path[id: id] else { return .none }
switch delegateAction {
case .deleteSyncUp:
state.syncUpsList.syncUps.remove(id: detailState.syncUp.id)
return .none

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

case .startMeeting:
state.path.append(.record(RecordMeeting.State(syncUp: detailState.syncUp)))
return .none
}
func deleteSyncUp(syncUp: SyncUp) {
state.syncUpsList.syncUps.removeAll {
$0.id == syncUp.id
}
}

func setPathRecord(id: String, record: RecordMeeting) {
switch delegateAction {
case let .save(transcript: transcript):
guard let id = state.path.ids.dropLast().last
else {
XCTFail(
"""
Record meeting is the only element in the stack. A detail feature should precede it.
"""
)
return .none
}

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

func syncUpUpdated(syncUp: SyncUp) {
if let i = state.syncUpsList.syncUps.firstIndex(where: { $0.id == syncUp.id }) {
state.syncUpsList.syncUps[i] = syncUp
}
}

func startMeeting(syncUp: SyncUp) {
state.path.record = RecordMeeting(syncUp: syncUp)
}
}

@Actions
extension Store<AppFeature>: RecordMeetingDelegate {

func savePath(transcript: String) {
guard let i = state.syncUpsList.syncUps.firstIndex(where: { $0.id == state.path.detail.syncUp.id }) else { return }
state.syncUpsList.syncUps[i] = state.path.detail.syncUp
}

func debounceSave(syncUps: [SyncUp]) async throws {
cancel(Self.debounceSave)
// try await di.clock.sleep(for: .seconds(1))
try await di.dataManager.save(JSONEncoder().encode(syncUps), .syncUps)
}
}

extension Store<AppFeature> {

var saveOnChange: Self {
onChange(of: \.syncUpsList.syncUps) { _, syncUps, _ in
Task {
try await debounceSave(syncUps: syncUps)
}
}
}

func debounceSave(syncUps: [SyncUp]) async throws {
cancel(Self.debounceSave)
try await di.clock.sleep(for: .seconds(1))
try await di.dataManager.save(JSONEncoder().encode(syncUps), .syncUps)
}
}

struct AppView: View {

@ViewStore var state: AppFeature


init(state: AppFeature) {
self.state = state
}

init(store: Store<AppFeature>) {
_state = ViewStore(store: store)
}

var body: some View {
NavigationStack(path: $state.binding.path.navigationPath) {
SyncUpsListView(store: $state.syncUpsList)
.navigationDestination($state.path.binding, for: \.$detail) {
SyncUpDetailView(store: $state.syncUpsList)
}
.navigationDestination($state.path.binding, \.$meeting) {
MeetingView(meeting: meeting, syncUp: syncUp)
}
.navigationDestination($state.path.binding, for: \.$record) {
RecordMeetingView(store: $state.syncUpsList)
}
NavigationSteps(selection: $state.binding.path.selected) {
listView
detailView

if state.path.selected == .record {
recordView
}
if state.path.selected == .meeting {
meetingView
}
}
.stepEnvironment($state.binding.path)
}

private var listView: some View {
SyncUpsListView(store: $state.syncUpsList)
.step($state.binding.path, \.$list)
}

private var detailView: some View {
SyncUpDetailView(
store: $state.path.detail
.di(\.syncUpDetailDelegate, $state)
)
.step($state.binding.path, \.$detail)
}

private var meetingView: some View {
MeetingView(
meeting: state.path.meeting.meeting,
syncUp: state.path.meeting.syncUp
)
.step($state.binding.path, \.$meeting)
}

private var recordView: some View {
RecordMeetingView(
store: $state.path.record
.di(\.recordMeetingDelegate, $state)
)
.step($state.binding.path, \.$record)
}
}

extension URL {
Expand Down
15 changes: 9 additions & 6 deletions Examples/SyncUps/SyncUps/Dependencies/DataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import VDStore
import Foundation

struct DataManager: Sendable {
var load: @Sendable (_ from: URL) throws -> Data
var load: @Sendable (_ from: URL) async throws -> Data
var save: @Sendable (Data, _ to: URL) async throws -> Void
}

Expand All @@ -12,29 +12,32 @@ extension DataManager {
save: { data, url in try data.write(to: url) }
)

static let testValue = Self()
static let testValue = Self { _ in
Data()
} save: { _, _ in
}
}

extension StoreDIValues {

@StoreDIValue
var dataManager: DataManager = valueFor(live: .liveValue, test: .testValue)
var dataManager: DataManager = valueFor(live: DataManager.liveValue, test: DataManager.testValue)
}

extension DataManager {

static func mock(initialData: Data? = nil) -> Self {
let data = LockIsolated(initialData)
let data = ActorIsolated(initialData)
return Self(
load: { _ in
guard let data = data.value
guard let data = await data.value
else {
struct FileNotFound: Error {}
throw FileNotFound()
}
return data
},
save: { newData, _ in data.setValue(newData) }
save: { newData, _ in await data.set(newData) }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ extension SpeechClient {
startTask: { _ in
AsyncThrowingStream { continuation in
Task { @MainActor in
await isRecording.setValue(true)
await isRecording.set(true)
var finalText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \
Expand Down
42 changes: 21 additions & 21 deletions Examples/SyncUps/SyncUps/Meeting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@ import VDStore
import SwiftUI

struct MeetingView: View {

let meeting: Meeting
let syncUp: SyncUp

var body: some View {
ScrollView {
VStack(alignment: .leading) {
Divider()
.padding(.bottom)
Text("Attendees")
.font(.headline)
ForEach(self.syncUp.attendees) { attendee in
Text(attendee.name)

let meeting: Meeting
let syncUp: SyncUp

var body: some View {
ScrollView {
VStack(alignment: .leading) {
Divider()
.padding(.bottom)
Text("Attendees")
.font(.headline)
ForEach(syncUp.attendees) { attendee in
Text(attendee.name)
}
Text("Transcript")
.font(.headline)
.padding(.top)
Text(meeting.transcript)
}
}
Text("Transcript")
.font(.headline)
.padding(.top)
Text(self.meeting.transcript)
}
.navigationTitle(Text(meeting.date, style: .date))
.padding()
}
.navigationTitle(Text(self.meeting.date, style: .date))
.padding()
}
}
9 changes: 5 additions & 4 deletions Examples/SyncUps/SyncUps/Models.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import SwiftUI
import Tagged

struct SyncUp: Equatable, Identifiable, Codable {

let id: Tagged<Self, UUID>
let id: UUID
var attendees: [Attendee] = []
var duration: Duration = .seconds(60 * 5)
var meetings: [Meeting] = []
Expand All @@ -16,14 +15,16 @@ struct SyncUp: Equatable, Identifiable, Codable {
}

struct Attendee: Equatable, Identifiable, Codable {
let id: Tagged<Self, UUID>
let id: UUID
var name = ""
}

struct Meeting: Equatable, Identifiable, Codable {
let id: Tagged<Self, UUID>
let id: UUID
let date: Date
var transcript: String

static let mock = Meeting(id: UUID(), date: Date(), transcript: "Lorem ipsum dolor sit amet")
}

enum Theme: String, CaseIterable, Equatable, Identifiable, Codable {
Expand Down
Loading

0 comments on commit 0d19a27

Please sign in to comment.