pteasima / Service

Sugar for SwiftUI.Environment

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Service

  • SwiftUI solves dependency injection in an ingenious, extensible way.
  • The EnvironmentValues struct can resolve services from arbitrary, even user defined EnvironmentKeys.
  • Furthermore, SwiftUI allows you to modify the environment at any point in the view tree. This means you can have different services injected into a subtree of Views.
  • So you can go nuts with configurations, should you need to, which you probably won’t.
  • So, what's a good service?
    • A good service can do anything.
    • By default, it stays in its domain and does what you'd expect.
    • But when developing or testing, you may want it to do all kinds of crazy things.
    • So it should have access to the whole environment, to be able to do anything.
@dynamicMemberLookup protocol Service: EnvironmentKey where Value == Self {
  init()
  var environment: EnvironmentValues { get set }
  typealias Endpoint<Action> = (EnvironmentValues) -> Action
  
  associatedtype Endpoints
  var endpoints: Endpoints { get }
}
extension Service {
  static var defaultValue: Self { .init() }
}

extension Service {
  subscript<Action>(dynamicMember keyPath: KeyPath<Endpoints, Endpoint<Action>>) -> Action {
    endpoints[keyPath: keyPath](environment)
  }
}

extension EnvironmentValues {
  subscript<S: Service>(service keyPath: KeyPath<S, S>) -> S {
    get {
      var instance = self[S.self]
      instance.environment = self
      return instance
    }
    set {
      self[S.self] = newValue
    }
  }
}

extension Environment where Value: Service {
  init() {
    self.init(\.[service: \Value.self])
  }
}

  • Wow, thats both stupid and stupid complicated 👍, but how is it to use?
struct Google: Service {
  var environment: EnvironmentValues = .init()
  var endpoints = (
    open: { environment in
      { query in
        environment.openURL(url(for: query))
      }
    } as Endpoint<(String) -> Void>,
    fetch: { environment in
      { query in
        //we're using `shared` session here, but we might as well retrieve one from the environment
        let session = URLSession.shared
        let (data, response) = try await session.data(from: url(for: query))
        return data
      }
    } as Endpoint<(String) async throws -> Data>
  )
  
  private static func url(for query: String) -> URL {
    var components = URLComponents(string: "https://www.google.com")!
    components.queryItems = [URLQueryItem(name: "q", value: query)]
    return components.url!
  }
}

struct GoogleView: View {
  @Environment() private var google: Google
  
  @State var query: String = ""
  @State var fetchedData: Result<Data, Error>?
  @State var currentRequest: Task<(), Never>?
  var body: some View {
    VStack {
      HStack {
        TextField("Search", text: $query)
        Button {
          currentRequest?.cancel()
          currentRequest = Task {
            fetchedData = await Result {
              try await google.fetch(query)
            }
          }
        } label: {
          Text("Fetch")
        }
        Button {
          google.open(query)
        } label: {
          Text("Open")
        }
      }
    }
    Text("Result: " + String(describing: fetchedData))
  }
}

// helper
extension Result where Failure == Error {
  init(catching body: () async throws -> Success) async {
    do {
      self = .success(try await body())
    } catch {
      self = .failure(error)
    }
  }
}

  • Why?
struct GoogleView_Previews: PreviewProvider {
  static var previews: some View {
    // default config
    GoogleView()
    // fetch from google but open in duckduckgo
    GoogleView()
      .environment(
        \.[service: \Google.self].endpoints.open,
         { environment in
           { query in
             var components = URLComponents(string: "https://www.duckduckgo.com")!
             components.queryItems = [URLQueryItem(name: "q", value: query)]
             environment.openURL(components.url!)
           }
         }
      )
  }
}

About

Sugar for SwiftUI.Environment


Languages

Language:Swift 100.0%