Skip to content

Commit

Permalink
Merge pull request #2 from SwiftedMind/feature/example-app-improvements
Browse files Browse the repository at this point in the history
Example app improvements
  • Loading branch information
SwiftedMind authored Jan 9, 2023
2 parents f109c33 + de62038 commit 33302c7
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@

import SwiftUI
import Puddles
import AsyncAlgorithms

struct Root: Coordinator {
@EnvironmentObject private var services: Services
@StateObject private var interface: Interface<Home.Action> = .init()
@StateObject private var helper: Helper = Helper()
@StateObject private var homeInterface: Interface<Home.Action> = .init()

@State private var events: HomeView.EventsLoadingState = .loaded(.repeating(.random, count: 5))
@State private var searchResults: HomeView.SearchResultsLoadingState = .initial

var entryView: some View {
Home(interface: interface, events: events, searchResults: searchResults)
Home(interface: homeInterface, events: events, searchResults: searchResults)
}

func modify(coordinator: CoordinatorContent) -> some View {
Expand All @@ -53,34 +55,47 @@ struct Root: Coordinator {
}

func interfaces() -> some InterfaceObservation {
AsyncInterfaceObserver(interface) { action in
await handleViewAction(action)
AsyncInterfaceObserver(homeInterface) { action in
await handleHomeAction(action)
}
AsyncInterfaceObserver(services.events.interface) { action in
await handleEventServiceAction(action)
AsyncChannelObserver(helper.searchChannel) { channel in
for await query in channel.debounce(for: .seconds(0-5)) {
searchEvents(query: query)
}
}
}

func handleViewAction(_ action: Home.Action) async {
func handleHomeAction(_ action: Home.Action) async {
switch action {
case .searchQueryUpdated(let query):
// Submit query to the event service, which takes care of debouncing
await services.events.submitSearchQuery(query)
await helper.searchChannel.send(query)
}
}

private func handleEventServiceAction(_ action: EventServiceAction) async {
switch action {
case .eventSearchChanged(let state):
switch state {
case .initial, .loading:
private func searchEvents(query: String) {
helper.searchTask?.cancel()

if query.isEmpty {
searchResults = .loaded([])
return
}

helper.searchTask = Task {
do {
searchResults = .loading
case .loaded(let events):
let events = try await services.events.searchEvents(query)
try Task.checkCancellation()
searchResults = .loaded(events)
case .failure:
break
} catch {
// TODO: Error Handling
}
}
}
}

extension Root {
@MainActor private class Helper: ObservableObject {
let searchChannel: AsyncChannel<String> = .init()
var searchTask: Task<Void, Never>?
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,47 +24,27 @@ import Foundation
import Puddles
import AsyncAlgorithms

public enum EventServiceAction {
case eventSearchChanged(LoadingState<[Event], Swift.Error>)
}

public protocol EventService {
@MainActor var interface: Interface<EventServiceAction> { get }
@MainActor func start() async
@Sendable func events() async throws -> [Event]
@Sendable func submitSearchQuery(_ query: String) async
@Sendable func searchEvents(_ query: String) async throws -> [Event]
}

public final class MockEventService: EventService {

@MainActor
public let interface: Interface<EventServiceAction> = .init()

private var events: [Event] = []

// Bad. multiple searches in the app will all go through this one channel, they all need their own! But how?
private var channel: AsyncChannel<String> = .init()

public init() {}

@MainActor
public func start() async {
for await _ in channel.debounce(for: .seconds(0.5)) {
if Task.isCancelled { return }
interface.sendAction(.eventSearchChanged(.loading))
try? await Task.sleep(nanoseconds: 2_000_000_000)
interface.sendAction(.eventSearchChanged(.loaded(.repeating(.random, count: 2))))
}
}

@MainActor
public func events() async throws -> [Event] {
.repeating(.random, count: 10)
try await Task.sleep(nanoseconds: 2_000_000_000)
return .repeating(.random, count: 10)
}

@MainActor
public func submitSearchQuery(_ query: String) async {
await channel.send(query)
public func searchEvents(_ query: String) async throws -> [Event] {
try await Task.sleep(nanoseconds: 2_000_000_000)
return [.init(name: "Mock Event: " + query)]
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,9 @@ import Foundation
public final class Services: ObservableObject {

public var events: EventService
private var eventsTask: Task<Void, Never>

public init(events: EventService) {
self.events = events

eventsTask = Task {
await events.start()
}
}

public func stop() {
eventsTask.cancel()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import SwiftUI
import AsyncAlgorithms

public struct AsyncChannelObserver<Value>: InterfaceObservation {

var channel: AsyncChannel<Value>
var actionHandler: @MainActor (_ channel: AsyncChannel<Value>) async -> Void

public init(
_ channel: AsyncChannel<Value>,
actionHandler: @MainActor @escaping (_ channel: AsyncChannel<Value>) async -> Void
) {
self.channel = channel
self.actionHandler = actionHandler
}
}

public extension AsyncChannelObserver {

@MainActor
var body: some View {
Color.clear
.task {
await actionHandler(channel)
}
}
}

0 comments on commit 33302c7

Please sign in to comment.