SwiftUI - Using an Array of Ints that is wrapped with #Binding - arrays

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())

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
}
}

Related

SwiftUI, IF inside ForEach loop to display different text

I am having a hard time displaying certain text in a ForEach loop in SwiftUI.
I have an array that I iterate over with ForEach. The problem is, I need to have certain text displayed for whether or not a specific date is selected in a calendar.
For example:
ForEach(reminders) { reminder in
if(Calendar.current.isDate(reminder.beginDate, equalTo: selectedPassedDate) {
HStack {
Text("The Text for the specific day")
} else {
Text("The text for the unspecific day")
}
}
}
When the array has more than 2 elements, the text for the non-matching day is being displayed multiple times (since it is inside the foreach). This makes sense, but I am trying to figure out a way to only display ONE of the "unspecified day" text instead of many.
I have tried adding a simple bool to display the other text, but I cannot change the state of it without a button or something.. I need to do this totally programmatically. I've also moved the else statement outside of the loop but again the secondary text is being displayed in the current day selection.
What can I do to solve this issue? If I can programmatically set the bool while conforming to SwiftUIs view then I can solve this, but I'm not sure how.
you could try something like this:
class FirstTime: ObservableObject {
var value = true
}
struct MyView: View {
...
#State var firstTime = FirstTime()
var body: some View {
VStack {
ForEach(reminders) { reminder in
HStack {
if (Calendar.current.isDate(reminder.beginDate, equalTo: selectedPassedDate) {
Text("The Text for the specific day")
} else {
if firstTime.value {
showTextOnce()
}
}
}
}
}
}
func showTextOnce() -> some View {
firstTime.value = false
return Text("The text for the unspecific day")
}
}

How to pass an array from a view to another and update the view accordingly

I have View1 that has a list, a Navigation button to View2, and an array with based on the items in the array, it will make rows.
In View2 I have a list of items.
I want the user to go to View2 and choose items then add them to the array that exist is View1. Then View1 gets updated with the new added items.
It problem is about passing items from one view to another. Also it would be great if View2 goes back to View1 when the user clicks an item.
This is what I tried to create which does not work:
View 1
struct ContentView: View {
#State private var newItems : Array = []
var body: some View {
NavigationView {
List(){
Section(header:
HStack{
Image(systemName: "plus.square.fill")
Text("Find Items")
})
{
NavigationLink( destination: MyListView(newItems: self.$newItems) ){
Text("Add Items")
}
}
Section(header:
HStack{ Text("Items") }
) {
ForEach(newItems, id: \.self){ item in
Text("\(item)")
}
}
}
.navigationBarTitle("Items List")
.listStyle(GroupedListStyle())
}
}
}
View 2
struct MyListView: View {
#Binding var newItems: Array = []
var myArray = [1, 2, 3, 4]
var body: some View {
List(myArray, id: \.self ){i in
HStack{
Image(systemName: "photo")
Text("\(i)").font(.headline)
}.onTapGesture {
self.newItems.append(i)
print(self.newItems.count)
}
}
}
}
Any idea how to solve this?
Thanks in advance
Change:
struct MyListView: View {
#Binding var newItems: Array = []
To:
struct MyListView: View {
#Binding var newItems: Array
The problem is that in your MyListView you are replacing the bound array with a different local empty array.
When you pass in a #Binding value you NEVER initialize it in the destination view!
The problem is that you're passing an array, which is a value type (arrays, strings, dictionaries, structs, etc. are value types). When you pass that array, you're (effectively) passing a new copy of the data in it to the new view. Changes you make there will not affect the array in your first View, and would have to then be passed back into the old View, which is a bit of a mess to keep in sync.
Instead you want to use a reference type (a class), where the variable passed to the new View is actually just a pointer to the same data (the class instance) that the first View's variable was pointing at. Then when ANY View or function works on that variable, it will edit the same instance data.
Super handy for passing data back and forth through multi-view apps. 👍
Only catch is that you cant use the #State wrapper on classes. But there's a fairly simple equivalent for classes.
You need to first create the class as conforming to the protocol ObservableObject , and then declare any of its properties as #Published. Then in your Views you instantiate the classes as #ObservedObjects
So break out your newItems into a class
class NewItems: ObservableObject {
#Published var items = [Int]()
// And any other properties you want to pass back and forth.
// Maybe even a complex Struct (which is a value type, but wrapped in a class)
}
And then in your ContentView, you have to instantiate that class and call its items array a bit differently:
struct ContentView: View {
#ObservedObject var newItems: NewItems() // New instance of NewItems is created.
// FYI your array is now accessed by calling newItems.items like so:
// newItems.items = [1, 2, 3]
var body: some View {
NavigationView {
List(){
Section(header:
HStack{
Image(systemName: "plus.square.fill")
Text("Find Items")
})
{
// Now you're passing a pointer to the instance of the NewItems class
NavigationLink( destination: MyListView(newItems: self.$newItems) ){
Text("Add Items")
}
}
Section(header:
HStack{ Text("Items") }
) {
ForEach(newItems, id: \.self){ item in
Text("\(item)")
}
}
}
.navigationBarTitle("Items List")
.listStyle(GroupedListStyle())
}
}
}
So you're passing an instance of the NewItems class to your MyListView and that means BOTH views will share the same data within that NewItems. Changes in one view will affect the other. Go back and forth all you want, make changes, it will just work.
Here's how you handle the new class instance in MyListView:
struct MyListView: View {
// We're getting a class now that has your [Int] within it
#ObservedObject var newItems: NewItems
var myArray = [1, 2, 3, 4]
var body: some View {
List(myArray, id: \.self ){i in
HStack{
Image(systemName: "photo")
Text("\(i)").font(.headline)
}.onTapGesture {
self.newItems.append(i)
// Make sure you call the items array within newItems
print(self.newItems.items.count)
}
}
}
}
To share the same data between multiple views (or multiple functions, etc) you can simply wrap it in a class. Then changes on any view will be made to the same data within that class instance.
You can even take it one step further and wrap the NewItems instance with #EnvironmentObject, so you don't even have to pass it between the views in your NavigationLinks. More details here: https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views

Why Can't I Append to a State Array Within View in SwiftUI?

I'm not sure why this code doesn't work. It builds and runs fine, but nothing is displayed in the view. While debugging, I can see that the append() calls don't actually add the items to the array, but there's no error:
import SwiftUI
struct Test: View {
#State private var array: [String] = []
var body: some View {
self.array.append("A")
self.array.append("B")
self.array.append("C")
return VStack {
ForEach(self.array, id: \.self) {string in
Text(string)
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
I've also tried doing the appends in an init(_:) with the same result. It works if I initialize the array with the values like #State private var array: [String] = ["A", "B", "C"] so it seems like there's some immutability of state variables nuance that I'm missing. I'm pretty new to Swift. Can someone please explain what I'm doing wrong here?
I tried to run your code on Xcode 11.4.1, I got a warning saying;
Modifying state during view update, this will cause undefined behavior
This error occurs because you’re trying to modify the state of a SwiftUI view while it is actually being rendered.
So as an alternative you can try appending items in onAppear block
struct Test: View {
State private var array: [String] = []
var body: some View {
VStack {
ForEach(self.array, id: \.self) {string in
Text(string)
}
}.onAppear { // Prefer, Life cycle method
self.array.append("A")
self.array.append("B")
self.array.append("C")
}
}
}
Then you'll be able to see your items in the screen.
Also here is a bit detailed explanation

How to use #EnvironmentObject to populate and update list automatically in SwiftUI?

Like the title says, I'm trying to use an #EnvironmentObject that holds data to populate and update a List automatically in SwiftUI.
For context, I'm building an app that is supposed to show a list of locations. The location data will be shown in multiple places of my app and will be changing during use, so I thought an #EnvironmentObject would be perfect for holding the data. But, I'm having trouble feeding an #EnvironmentObject to populate a List() and having the list update as the #EnvironmentObject changes.
Below is the struct I created for the location data I want to display:
struct ListVenue : Identifiable {
var id = UUID()
var name: String
var formatted_address : String?
var website : String?
}
Below is my SceneDelegate.swift file, where I create the #EnvironmentObject and the class it references:
import UIKit
import SwiftUI
class venDataArray: ObservableObject {
var array : [ListVenue] = [ListVenue(name: "test_name")]
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
#EnvironmentObject var VDArray: venDataArray
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().environmentObject(VDArray)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
And, here is the my ContentView.swift file where I want to use the #EnvironmentObject above to populate and update a List() automatically:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var VDArray: venDataArray
var body: some View {
VStack(alignment: .leading) {
Text("Location List")
// This is where im failing at having a list take in an #EnvironmentObject
List(VDArray) { ListVenue in
// vvv This is the view I want displayed for each item in the #EnvironmentObject vvv
VenueRowTest()
}
}
}
Can anyone show me how to alter my code so that I can display and update data in a list using an #EnvironmentObject??
Or is there a better way to implement a dynamic List?
Have a look at the following tutorials:
How to use environmentobject to share data
Apple - handling user inputs
First You need a #Published property wrapper for your code to work.
class VenDataArray: ObservableObject {
#Published var array : [ListVenue] = [ListVenue(name: "test_name")]
}
Than adjust yout Scene delegate
var window: UIWindow?
var vDArray = VenDataArray()
let contentView = ContentView().environmentObject(vDArray)
Note: I have adjusted the variables with lowerCamelCase acc. to the API design guidelines

How to pass data into swiftui view and access it

Im trying to pass data into a swiftui view to be displayed when it is initialized, but am having trouble getting it to work.
Here is my code which passes data into my 'MarkerInfoWindow.swift' view:
func mapView(_ mapView: GMSMapView, markerInfoContents marker: GMSMarker) -> UIView? {
print("Showing marker infowindow")
print("marker.userData: \(marker.userData)")
let mUserData = marker.userData as? [String:String]
print("mUserData?['name']", (mUserData?["name"]) ?? "mUserData[name] was nil")
let mInfoWindow = UIHostingController(rootView: MarkerInfoWindow(placedata: mUserData!))
return mInfoWindow.view
}
Here is the code to my 'MarkerInfoWindow.swift' view:
struct PlaceDataStruct {
var name : String
var place_id : String
}
struct MarkerInfoWindow: View {
var placedata: [PlaceDataStruct]
var body: some View {
//Here is where i keep getting errors regardless if i use this method or dot notation
Text(placedata["name"])
}
}
Im not sure if im implementing my PlaceDataStruct incorrectly.
Does anyone know what I'm doing wrong, so that I can display the right data every time my view is initialized?
You’re MarkerInfoWindow is expecting an array of structs but the data you are passing is a dictionary, in the form [String: String]
You should update your MarkerInfoWindow to accept a dictionary instead. Here is an example of how to do that.
struct MarkerInfoWindow: View {
var placedata: [String: String] // update to be dictionary
var body: some View {
Text(placedata["name", default: ""]) // use correct notation for accessing dictionary
}
}
You’re also force unwrapping the data before you pass it to your MarkerInfoWindow if that’s your intention that is ok. But note that if your data isn’t there then it will crash your app.

Resources