I gather that the @Binding wrapper is used when a parent view and a child view both need to share a variable without violating the principle of having “a single source of truth”. This isn’t working as I expect when the variable is an Array. Essentially, the data that the child view assigns to this bound variable gets dropped so that the parent view sees an empty array.
The attached example is taken from the Developer Documentation (under XCode’s Help menu) for the @Binding wrapper. Apple’s code shows a simple video controller. It allows a user to play or pause videos (videos are stored as Episode
structs). The child view is named PlayButton
and toggles between a right-arrow (start) and a double-bar (stop). In Apple’s code the child view had a single variable wrapped with @Binding, which was the isPlaying
Boolean variable. When the play button is tapped it toggles the isPlaying
variable. As you might expect, the orginal code worked fine.
To demonstrate the problem I’ve modified the child view so that now accepts an array of Episodes. Please assume that the child view must report the size of the show titles to the parent view without violating the single source of truth principle. As a consequence of this need, there is also a new @Binding-wrapped array that records the number-of-characters in a show’s title (for each of the shows).
Additionally, the child view now shows Text Views reporting the show title of each Episode and the title’s size, just to prove that the code is being executed. The parent View should then be able to display the number of elements in its @State array, sizeOfShowTitles
. As coded here, I expect the number of episodes to be 1. Instead, the parent view is reporting that sizeOfShowTitles has zero elements.
The playground contains just these 4 elements:
- Episode (a struct that identifies the videos)
- PlayButton (the child View)
- PlayerView (the parent View)
- the PlaygroundPage.current.setLiveView(PlayerView()) command, used to excuted SwiftUI in playgrounds.
Can anyone comment on why assignment of values to the bound array is failing?
Note 1:
The problem does not seem to lie with the .append
function used in the child view. If you replace the append function with a simple assignment then the outcome is the same - there are no elements in the array in the parent View (tested, but not shown here).
Note 2. As shown in Apples code (and retained here), assigning a value to a Boolean Type works as expected. So, it appears that the problem has something to do with the Array Type.
Note 3. I’m using Swift 5.5 in XCode 5.4.1.
// Playground used to show the issue with saving a
// value to an array that is wrapped with @Binding
import SwiftUI
import PlaygroundSupport
// HELPER Struct: records metadata on shows that can be played in PlayerView
struct Episode: Identifiable {
var id = UUID()
var title = "Title"
var showTitle = "Show Title"
}
// CHILD View
// String Array)
struct PlayButton: View {
// input API
@Binding var isPlaying: Bool
var episodes: [ Episode ]
// output API
@Binding var charactersInShowTitle: [ Int ]
var body: some View {
Button(action: {
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
ForEach( episodes ) { episode in
Text( "CHILD: \( analyzeShowTitle( episode) )" )
}
}
func analyzeShowTitle( _ episode: Episode ) -> String {
let characterCount = episode.showTitle.count
charactersInShowTitle.append( characterCount )
return "\( episode.showTitle ) - \( characterCount ) chars"
}
}
// PARENT
// (modified to show the list of sizes from a String Array)
struct PlayerView: View {
let episodes = [ Episode(title: "Title 1",
showTitle: "Show 1"),
Episode( title: "Title 1",
showTitle: "Show 21")
]
@State private var isPlaying: Bool = false
@State private var sizeOfShowTitles = [ Int ]()
var body: some View {
VStack {
Text("Player App")
.foregroundStyle(isPlaying ? .primary : .secondary)
Text("")
PlayButton(isPlaying: $isPlaying,
episodes: episodes,
charactersInShowTitle: $sizeOfShowTitles )
Text("")
Text( "PARENT no. elements: \( $sizeOfShowTitles.wrappedValue.count)"
)
}
.frame(width: 300)
}
}
PlaygroundPage.current.setLiveView(PlayerView())
CodePudding user response:
How about trying something like this example code,
using .onAppear{...}
for updating each episode with the showTitle.count
.
Also removing this "appending" from analyzeShowTitle
function. This append(...)
should not be used within the ForEach
loop, because it triggers a view refresh,
which then triggers another append(...)
etc...
struct PlayButton: View {
// input API
@Binding var isPlaying: Bool
var episodes: [Episode]
// output API
@Binding var charactersInShowTitle: [Int]
var body: some View {
Button(action: {
isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
ForEach(episodes) { episode in
Text("CHILD: \(analyzeShowTitle(episode))")
}
.onAppear { // <-- here
for episode in episodes {
charactersInShowTitle.append(episode.showTitle.count)
}
}
}
func analyzeShowTitle( _ episode: Episode ) -> String {
return "\( episode.showTitle ) - \( episode.showTitle.count ) chars" // <-- here
}
}