Reputation: 680
Let’s say I have a model class Ball
, that conforms to the ObservableObject
protocol, and I define an optional instance of it (var ball: Ball?
).
Is there a way to trigger a NavigationLink
to display its destination
view based on setting the value of the optional instance? So when I self.ball = Ball()
, a NavigationLink
will trigger?
One problem seems to be that an optional (Type?
) can’t be an @ObservedObject
.
The other problem seems to be that the isActive:
parameter for NavigationLink
can only take a Binding<Bool>
.
/// Contrived minimal example to illustrate the problem.
include SwiftUI
class Ball: ObservableObject {
@Published var colour: String = "red"
// ...
}
struct ContentView: View {
// this won’t trigger view updates when it’s set because it’s not observed:
var ball: Ball?
// this line won’t compile:
@ObservedObject var observedBall: Ball?
// 🛑 Property type 'Ball?' does not match that of the
// 'wrappedValue' property of its wrapper type 'ObservedObject'
var body: some View {
NavigationView {
VStack {
// I want this to navigate to the ballView when isActive becomes true,
// but it won’t accept the test of nil state on the optional value:
NavigationLink(
destination: BallView(ball: self.ball), isActive: self.ball != nil
) {
EmptyView()
} // 🛑 compiler error because `self.ball != nil` isn’t valid for `isActive:`
// Button user taps to set the `ball`,
// which I want to trigger the BallView to be shown.
Button(action: { self.ball = Ball() }, label: { Text("Show Ball") })
}
}
}
}
struct BallView: View {
@ObservedObject var ball: Ball
// typical view stuff here ...
}
Upvotes: 3
Views: 5118
Reputation: 19
I encountered similar problem and the way I tried to get my NavigationLink
to render based on a nullable object is to wrap around the NavigationLink
if a optional binding to that optional object, then attach the onAppear
callback to modify the isActive
binding boolean to turn on the navigation link (so that it become active after it added to the view hierarchy and thus keep the transition animation)
class ObservableBall: ObservableObject {
@Published var ball: Ball?
@Published var showBallView = false
}
struct ContentView: View {
@ObservedObject var observedBall: ObservableBall
var body: some View {
NavigationView {
VStack {
if let ball = observedBall.ball {
NavigationLink(
destination: BallView(ball: ball),
isActive: $observedBall.showBallView)
{
EmptyView()
}
.onAppear {
observedBall.showBallView = true
}
}
// Button user taps to set the `ball`,
// which I want to trigger the BallView to be shown.
Button(action: { self.observedBall.ball = Ball() }, label: { Text("Show Ball") })
}
}
}
}
struct BallView: View {
@ObservedObject var ball: Ball
// typical view stuff here ...
}
Upvotes: 0
Reputation: 1089
You can use the init(get:set:) initializer of the Binding
to activate the destination view based on the optional instance or on an arbitrary conditional logic. For example:
struct Ball {
var colour: String = "red"
// ...
}
struct ContentView: View {
@State private var ball: Ball?
var body: some View {
NavigationLink(isActive: Binding<Bool>(get: {
ball != nil
}, set: { _ in
ball = nil
})) {
DestinationView()
} label: {
EmptyView()
}
}
}
I've used struct Ball
instead of class Ball: ObservableObject
, since @ObservedObject ball: Ball?
represents a value semantic of type Optional<Ball>
, thus cannot be combined with @ObservedObject
or @StateObject
, which are property wrappers for reference types. The value types (i.e., Optinal<Ball>
) can be used with @State
or @Binding
, but this is most likely not what you want with an optional observable object.
Upvotes: 0
Reputation: 680
So far, the best workaround I have for the above limitations involves:
@State var ballIsSet = false
(or @Binding var ballIsSet: Bool
) variable to my view,didSet
function on the wrapped Ball variable that updates the passed in boolean.Phew!
Hopefully someone knows a simpler/better way to do this…
// Still a somewhat contrived example, but it illustrates the points…
class ObservableBall: ObservableObject {
// a closure to call when the ball variable is set
private var whenSetClosure: ((Bool) -> Void)?
@Published var ball: Ball? {
didSet {
// if the closure is assigned, call it when the ball gets set
if let setClosure = self.whenSetClosure {
setClosure(self.ball != nil)
}
}
}
init(_ ball: Ball? = nil, whenSetClosure: ((Bool) -> Void)? = nil) {
self.ball = ball
self.whenSetClosure = whenSetClosure
}
}
struct ContentView: View {
@ObservedObject var observedBall = ObservableBall()
@State var ballIsSet = false
var body: some View {
NavigationView {
VStack {
// Navigate to the ballView when ballIsSet becomes true:
NavigationLink(
// we can force unwrap observedBall.ball! here
// because this only gets called if ballIsSet is true:
destination: BallView(ball: self.observedBall.ball!),
isActive: $ballIsSet
) {
EmptyView()
}
// Button user taps to set the `ball`,
// triggering the BallView to be shown.
Button(
action: { self.observedBall.ball = Ball() },
label: { Text("Show Ball") }
)
}
.onAppear {
observedBall.whenSetClosure = { isSet in
self.ballIsSet = isSet
}
}
}
}
}
Upvotes: 0