Skip to content

Instantly share code, notes, and snippets.

@DeFrenZ
Created November 14, 2020 23:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DeFrenZ/cce61facb35f6b314ef953eb8a05750f to your computer and use it in GitHub Desktop.
Save DeFrenZ/cce61facb35f6b314ef953eb8a05750f to your computer and use it in GitHub Desktop.
Dependency Injection a-la SwiftUI.Environment style by (ab)using the responder chain
import UIKit
@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
private let testService: TestService = .init()
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration
{
setEnvironmentValue(testService, for: \.testService)
return UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role)
}
}
// Taken https://stackoverflow.com/a/63738419/1288097 as a base
/*
AssociatedObject.swift
Copyright © 2020 RFUI.
https://github.com/BB9z/iOS-Project-Template
The MIT License
https://opensource.org/licenses/MIT
*/
import Foundation
/**
Objective-C associated value wrapper.
Usage
```
private let fooAssociation = AssociatedObject<String>()
extension SomeObject {
var foo: String? {
get { fooAssociation[self] }
set { fooAssociation[self] = newValue }
}
}
```
*/
public final class AssociatedObject<T> {
private let policy: objc_AssociationPolicy
/// Creates an associated value wrapper.
/// - Parameter policy: The policy for the association.
public init(policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) {
self.policy = policy
}
/// Accesses the associated value.
/// - Parameter index: The source object for the association.
public subscript(index: AnyObject) -> T? {
get { objc_getAssociatedObject(index, associatedObjectKey) as? T }
set { objc_setAssociatedObject(index, associatedObjectKey, newValue, policy) }
}
private var associatedObjectKey: UnsafeMutableRawPointer {
Unmanaged.passUnretained(self).toOpaque()
}
}
import UIKit
final class RootViewController: UIViewController {
@ResponderEnvironment(\.testService) var testService
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(testService.value)
}
}
import UIKit
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions)
{
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = RootViewController()
self.window = window
window.makeKeyAndVisible()
}
}
import Foundation
import Combine
final class TestService {
@Published
private(set) var value: Int = 0
}
// MARK: - ResponderEnvironment
private let testServiceAssociation: AssociatedObject<TestService> = .init()
extension ResponderEnvironmentStorage {
var testService: TestService? {
get { testServiceAssociation[self] }
set { testServiceAssociation[self] = newValue }
}
}
import UIKit
@propertyWrapper
struct ResponderEnvironment<Value> {
private let key: Key
init(_ key: Key) {
self.key = key
}
var wrappedValue: Value {
fatalError()
}
static subscript <EnclosingSelf: UIResponder> (
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: Key,
storage storageKeyPath: KeyPath<EnclosingSelf, Self>) -> Value
{
object.environmentValue(wrappedKeyPath)
}
typealias Key = KeyPath<Storage, Value?>
}
extension ResponderEnvironment {
typealias Storage = ResponderEnvironmentStorage
}
final class ResponderEnvironmentStorage: NSObject {
}
private let responderEnvironmentStorageAssociation: AssociatedObject<ResponderEnvironmentStorage> = .init()
extension UIResponder {
fileprivate var storage: ResponderEnvironmentStorage {
get {
if let storage = responderEnvironmentStorageAssociation[self] { return storage }
let newStorage = ResponderEnvironmentStorage()
self.storage = newStorage
return newStorage
}
set { responderEnvironmentStorageAssociation[self] = newValue }
}
fileprivate func environmentValue <Value> (_ key: KeyPath<ResponderEnvironmentStorage, Value?>) -> Value! {
storage[keyPath: key]
?? next?.environmentValue(key)
}
func setEnvironmentValue <Value> (
_ value: Value,
for key: WritableKeyPath<ResponderEnvironmentStorage, Value>)
{
storage[keyPath: key] = value
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment