Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve DeepLinking by accessing the stored value of @Published properties directly #7

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 10 additions & 32 deletions Sources/XUI/DeepLink/DeepLinkable+Receiver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,25 @@ extension DeepLinkable {
as type: Receiver.Type,
where filter: (Receiver) -> Bool = { _ in true }
) -> Receiver! {
firstReceiver(ofType: type, where: filter) { $0.children }
?? firstReceiver(ofType: type, where: filter) {
Mirror(reflecting: $0)
.values { $0 as? DeepLinkable }
}
}

}

extension DeepLinkable {

// MARK: Helpers - DeepLink

private func firstReceiver<Receiver>(
ofType type: Receiver.Type,
where filter: (Receiver) -> Bool,
children: (DeepLinkable) -> [DeepLinkable]
) -> Receiver? {
var visited = Set<ObjectIdentifier>()
var stack = [DeepLinkable]()
return firstReceiver(
where: { ($0 as? Receiver).map(filter) ?? false },
visited: &visited,
stack: &stack,
children: children
stack: &stack
) as? Receiver
}

}

extension DeepLinkable {

// MARK: Helpers - DeepLink

private func firstReceiver(
where filter: (Any) -> Bool,
visited: inout Set<ObjectIdentifier>,
stack: inout [DeepLinkable],
children: (DeepLinkable) -> [DeepLinkable]
stack: inout [DeepLinkable]
) -> Any? {

visited.insert(ObjectIdentifier(self))
Expand All @@ -54,7 +40,7 @@ extension DeepLinkable {
}

stack.append(
contentsOf: children(self)
contentsOf: children
.filter { !visited.contains(ObjectIdentifier($0)) }
)

Expand All @@ -64,16 +50,8 @@ extension DeepLinkable {

stack.removeFirst()
return next
.firstReceiver(where: filter, visited: &visited, stack: &stack, children: children)
.firstReceiver(where: filter, visited: &visited, stack: &stack)
}

}

extension Mirror {

fileprivate func values<Value>(_ compactMap: (Any) -> Value?) -> [Value] {
children.compactMap { compactMap($0.value) }
+ (superclassMirror?.values(compactMap) ?? [])
}

}
25 changes: 22 additions & 3 deletions Sources/XUI/DeepLink/DeepLinkable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,33 @@
//

public protocol DeepLinkable: AnyObject {
@DeepLinkableBuilder var children: [DeepLinkable] { get }
var children: [DeepLinkable] { get }
}

extension DeepLinkable {

@DeepLinkableBuilder
public var children: [DeepLinkable] {
get {}
Mirror(reflecting: self)
.values(extractDeepLinkable)
}

private func extractDeepLinkable(from object: Any) -> DeepLinkable? {
if let value = object as? DeepLinkable {
return value
} else if let value = object as? DeepLinkableWrapper {
return extractDeepLinkable(from: value.content)
} else {
return nil
}
}

}

extension Mirror {

fileprivate func values<Value>(_ compactMap: (Any) -> Value?) -> [Value] {
children.compactMap { compactMap($0.value) }
+ (superclassMirror?.values(compactMap) ?? [])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename the compactMap closure as it's not a compact map, it's the function that is applied by compact map (but not only!).

Suggestion: mappingFunction or transform.

(I know this code was just moved, but I only noticed it now.)

Also, this is a good example where more code is easier to read:

func values<Value>(_ mappingFunction: (Any) -> Value?) -> [Value] {
    let values = children.compactMap { mappingFunction($0.value) }

    if let superClassValues = superclassMirror?.values(mappingFunction) {
        return values + superClassValues
    } else {
        return values
    }
}

This makes it much more visible when recursion occurs and which is the recursion anchor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Also not sure about the function name values but I see where it comes from and I don't have a better idea right now.)

}

}
66 changes: 0 additions & 66 deletions Sources/XUI/DeepLink/DeepLinkableBuilder.swift

This file was deleted.

47 changes: 47 additions & 0 deletions Sources/XUI/DeepLink/DeepLinkableWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// DeepLinkableWrapper.swift
// XUI
//
// Created by Paul Kraft on 01.03.21.
// Copyright © 2021 QuickBird Studios. All rights reserved.
//

public protocol DeepLinkableWrapper {
var content: Any { get }
}

extension Published: DeepLinkableWrapper {

public var content: Any {
guard let value = Mirror(reflecting: self).descendant("storage", "value") else {
assertionFailure("We could not extract a wrappedValue from a Published property wrapper.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who's "we"? 😅

return ()
}
return value
}

}

extension Binding: DeepLinkableWrapper {
public var content: Any { wrappedValue }
}

extension State: DeepLinkableWrapper {
public var content: Any { wrappedValue }
}

extension ObservedObject: DeepLinkableWrapper {
public var content: Any { wrappedValue }
}

extension EnvironmentObject: DeepLinkableWrapper {
public var content: Any { wrappedValue }
}

extension Store: DeepLinkableWrapper {
public var content: Any { wrappedValue }
}

extension Optional: DeepLinkableWrapper {
public var content: Any { flatMap { $0 } ?? () }
}
28 changes: 28 additions & 0 deletions Tests/XUITests/DeepLinkTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

import XCTest
@testable import XUI

class ParentViewModel: ViewModel, ObservableObject {
@Published var child: ChildViewModel?
}

class ChildViewModel: ViewModel, ObservableObject {

}

final class DeepLinkTests: XCTestCase {

func testMirror() {
let viewModel = ParentViewModel()
XCTAssertNil(viewModel.firstReceiver(as: ChildViewModel.self))
viewModel.child = .init()
XCTAssertNotNil(viewModel.firstReceiver(as: ChildViewModel.self))
viewModel.child = nil
XCTAssertNil(viewModel.firstReceiver(as: ChildViewModel.self))
}

static var allTests = [
("testMirror", testMirror),
]

}
1 change: 1 addition & 0 deletions Tests/XUITests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(DeepLinkTests.allTests),
testCase(XUITests.allTests),
]
}
Expand Down