-
-
Notifications
You must be signed in to change notification settings - Fork 14
Turbo navigator without session #74
Turbo navigator without session #74
Conversation
Exciting! Is the goal to figure out an approach to #69 or code quality? |
This is an approach to #69! |
* Ensure one is always set * Expose an open, default implementation * Implement optional methods in extension
This is looking great! I just pushed some changes that:
|
public var rootViewController: UIViewController { navigationController } | ||
public let navigationController: UINavigationController | ||
public let modalNavigationController: UINavigationController | ||
defer { self.webkitUIDelegate = TurboWKUIController(delegate: self) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defer
is needed to trigger the didSet
callback above. I don't like this but can't think of another way without duplicating the logic in that callback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind duplicating the logic. It really should just be about setting the delegate to both sessions and that's what we're doing here already. I don't feel strongly about it, though.
navigationController.presentedViewController != nil ? modalNavigationController : navigationController | ||
} | ||
|
||
var animationsEnabled: Bool = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this exist? I don't see a way to set it publicly as a consumer of the library. If anything, I'd prefer to see this live in the path configuration somehow so individual links can be customized instead of a global option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha. That was a hack. There's no way to pass along a type of UINavigationController
into this class anymore, so testing was broken. What I did was create this variable and keep it internal so consumers cannot use it but unit tests can reach it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, nice catch! What about exposing the navigation controllers in the initializer but defaulted like we had before?
init(delegate: TurboNavigationHierarchyControllerDelegate, navigationController: UINavigationController = UINavigationController(), modalNavigationController: UINavigationController = UINavigationController()) {
self.delegate = delegate
self.navigationController = navigationController
self.modalNavigationController = modalNavigationController
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think that's the way forward. I originally didn't go with this because I didn't want to expose them, but since this class is now internal, I don't see any issues at all.
public var webkitUIDelegate: TurboWKUIController? { | ||
didSet { | ||
session.webView.uiDelegate = webkitUIDelegate | ||
modalSession.webView.uiDelegate = webkitUIDelegate | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh should we make this public var webkitUIDelegate: WKUIDelegate?
instead here? Otherwise I think we run into the issue @joemasilotti raised here about having to keep our TurboWKUIController
in step with the WKUIDelegate protocol that we don't control, because subclasses can't implement protocol methods that aren't in the superclass.
And it'd be nice for users to be able to opt out of the alerting behaviour if they want, by supplying their own WKUIDelegate.
Perhaps we could set the default TurboWKUIController up as part of the convenience init
on TurboNavigator, if the intention there is to provide the easiest route for set up 🤔?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch! One caveat is that TurboWKUIController
is a class, not a protocol. So a developer can subclass it to customize alert/confirm behavior or add new functionality.
But doing so is a little awkward because it needs a reference back to TurboNavigator
:
self.turboNavigator = TurboNavigator(pathConfiguration: self.pathConfiguration)
self.turboNavigator.webkitUIDelegate = WKUIController(delegate: self.turboNavigator)
class WKUIController: TurboWKUIController {
// Override functions to customize behavior, presenting alerts on `delegate`.
}
If we expose a generic WKUIDelegate
then I don't see how a developer can grab a reference back to the TurboWKUIDelegate
needed to actually do the alert presenting!
Here's the diff I'm working with to allow subclassing:
diff --git a/Sources/TurboWKUIDelegate.swift b/Sources/TurboWKUIDelegate.swift
index 94eb1d4..f14ebba 100644
--- a/Sources/TurboWKUIDelegate.swift
+++ b/Sources/TurboWKUIDelegate.swift
@@ -5,9 +5,10 @@ public protocol TurboWKUIDelegate : AnyObject {
func present(_ alert: UIAlertController, animated: Bool)
}
-public class TurboWKUIController : NSObject, WKUIDelegate {
- weak var delegate: TurboWKUIDelegate?
- init(delegate: TurboWKUIDelegate!) {
+open class TurboWKUIController: NSObject, WKUIDelegate {
+ public private(set) weak var delegate: TurboWKUIDelegate?
+
+ public init(delegate: TurboWKUIDelegate!) {
self.delegate = delegate
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh that's a good point about the delegate. Would wrapping like this work do you reckon? So you'd be able to pass in either a subclassed TurboWKUIController
or your own WKUIDelegate
and have either be accessible from the outside?
public enum WKUIDelegateType {
case standardBehaviour(TurboWKUIController)
case custom(WKUIDelegate)
var delegate: WKUIDelegate {
switch self {
case .custom(let delegate): return delegate
case .standardBehaviour(let controller): return controller
}
}
}
...
public var webkitUIDelegate: WKUIDelegateType? {
didSet {
session.webView.uiDelegate = webkitUIDelegate?.delegate
modalSession.webView.uiDelegate = webkitUIDelegate?.delegate
}
}
... // then in the init method:
defer { self.webkitUIDelegate = .standardBehaviour(TurboWKUIController(delegate: self)) }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh should we make this public var webkitUIDelegate: WKUIDelegate? instead here?
I went with a class implementing WKUIDelegate
so users could subclass it and override any protocol function in WKUIDelegate. If we explosed WKUIDelegate
we leave all the responsibility of catching and building alerts to the consumer. We should definitely allow that flexibility, just not as a default. TurboWKUIController
is the default.
Having said that...
subclasses can't implement protocol methods that aren't in the superclass.
This is an interesting idea! I see WKUIDelegate
is an Obj-C protocol, so your observation is great: what happens if TurboWKUIDelegate
does not conform to an optional method? Can users override it? I actually didn't know if this was true or not so I just tested it in a Playground and I don't think it's entirely true.
import UIKit
@objc protocol TestProtocol {
@objc optional func doSomething()
@objc optional func saySomething()
}
class MyClassA : TestProtocol {
func doSomething() {
print("Do A")
}
}
class MyClassB : MyClassA {
override func doSomething() {
print("Do B")
}
func saySomething() {
print("Say")
}
}
let classA = MyClassA()
let classB = MyClassB()
classA.doSomething()
classA.saySomething?() // ❌ Value of type 'MyClassA' has no member 'saySomething'
classB.saySomething()
classB.doSomething()
*Note that MyClassB.doSomething
is overridden but MyClassB.saySomething
is not.
If you comment out classA.saySomething?()
the code compiles and behaves as expected. This is good news as it means we don't actually need to implement all methods in WKUIDelegate
. We can conform to however many we want, and users could just conform to functions that we don't implement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh that's a good point about the delegate. Would wrapping like this work do you reckon? So you'd be able to pass in either a subclassed TurboWKUIController or your own WKUIDelegate and have either be accessible from the outside?
I don't think I'm understanding the idea here. Why are we adding another layer of indirection via the enum? In my mind, a user would just subclass TurboWKUIDelegate
and just pass their instance as the controller. As soon as it's passed, it's set as the WKUI delegate.
If the issue is the default, we can just have TurboNavigator build its own TurboWKUIDelegate
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just pushed two commits that refactor both delegates a bit. I think this should get us everything we need. Check out the ComplexSceneDelegate
at the bottom of SceneDelegate.swift
.
The changes move the default implementation of TurboNavigatorDelegate
to a protocol extension. Which means developers only have to implement the functions they want.
And makes TurboWKUIController
open for subclassing, allowing a developer to customize only what they need.
I'm pretty happy with this! And I think ComplexSceneDelegate
shows how it all works together. Let me know if I missed something or drifted off target.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@olivaresf Ohhhh you're totally right, apologies for the detour - I'd got mixed up with another subclassing/protocol scenario which I'll no doubt bore you with over coffee sometime 🙇♀️
@joemasilotti The new commits look great – I'm with Fernando, let's start to upstream and get real 😃
/// - Parameters: | ||
/// - pathConfiguration: | ||
/// - delegate: an optional delegate to handle custom view controllers | ||
public convenience init(pathConfiguration: PathConfiguration, delegate: TurboNavigatorDelegate? = nil) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really, really like this initializer. I think it's now very clear that the only required dependency for TurboNavigator
is the path configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Building on that… I don't think it is unreasonable for us to provide a default path configuration if one isn't provided. One that handles the required routing, only.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { | ||
guard let windowScene = scene as? UIWindowScene else { return } | ||
|
||
TurboConfig.shared.userAgent += " CUSTOM STRADA USER AGENT" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely something to consider later, but I think this is the last piece of the puzzle that I can't make fit. I don't like the static configuration. I know Android has a WebView subclass due to technical constraints. Maybe we can make it work somehow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. This static configuration made more sense (to me) before Strada existed and I was only setting the user agent. Honestly, we could even rip this out of turbo-ios (i.e. never upstream it) and force folks to use the Session
initializer if they want to customize the web view. But I'll see how that plays out in the upstream PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we have an instance in HEY where our composer's WebView is separate from the rest of the app and does not support the same Strada components as the rest of the app, so the user-agent is actually different for that webview. So we should support continue to support situations where the apps can set the user agent per WebView/Session
— but that doesn't need to be the default path for simpler apps.
On the Android side, we just notify the app see whenever the session is (re)created and can update the user agent directly for the webView
: https://github.com/hotwired/turbo-android/blob/main/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainSessionNavHostFragment.kt#L49
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Both the user agent and the web view subclass should be supported. I was just saying there should be a better way of achieving both goals than a static configuration.
I think we're in a great spot to get this upstreamed to Turbo. I think it was a great idea to have a I'm happy we could make this architecture work. It feels very flexible! 🙌🏻 |
Exciting! And I agree. This feels like the last bit of unknowns. |
This PR provided enough guidance to start upstreaming Turbo Navigator into turbo-ios. Follow progress on #158! |
Experimenting with breaking apart TurboNavigator into two classes:
TurboNavigator
(which handles session logic) andTurboNavigationHierarchyController
(which handles UIKit nav stack manipulation).