How to pass #State var between views? - swift

I wan't to have a view, where I can't select a date, then I want to click on a button, which opens GraphView, and sends the #State var date to the other view, so I can use the date var in the GraphView.
This is what I have tried until now, but it shows the error:
Argument passed to call that takes no arguments
struct ContentView: View {
#State var showGraph = false
var dateFormatterMonth: DateFormatter {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("M")
return formatter
}
#State var date = Date()
var body: some View {
Button(action: {
self.showGraph.toggle()
}) {
Text("Show Graph")
}.sheet(isPresented: $showGraph){
GraphView(date: date) {
}
}
if showGraph {
GraphView()
}
else {
DatePicker("Date", selection: $date, displayedComponents: .date)
.frame(width: 100, height: 20, alignment: .center)
Text("Date is \(date, formatter: dateFormatterMonth)")
}

You need to use Binding in GraphView, like
struct GraphView: View {
#Binding var date: Date // << here !!
...
}
and pass it like,
Button(action: {
self.showGraph.toggle()
}) {
Text("Show Graph")
}.sheet(isPresented: $showGraph){
GraphView(date: $date) { // << here !!
}
}
if showGraph {
GraphView(date: $date) // << here !!
}

Your error seems to be that you are trying to pass an argument to a initialiser without one. You can change your GraphView to something like
struct GraphView: View {
#Binding var date: Date
var body: some View { ... }
}
you can then call it like this GraphView(date: $date).
That should do it.

If you want a "global" variable that can be used in multiple views you could consider using #EnvironmentObject.
Therefore you would have to define a new class in which you include date as #Published var. Then you would have to inject the environment object in your view(s) and can add it as #EnvironmentObject.
Here you can find more information: https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views
In order to make preview in Xcode work with environment objects, remember to also add it to the View in the preview code using .environmentObject()

Related

SwiftUI: Preventing binding value from passing back up?

in the Secondary struct, the #Binding property is secondTime and I want it to initially have the value from the "parent".
But when I change the value in this struct, the time property in the parent also changes. Is there a way to get the value from the parent but prevent any changes to the value from going back up to the parent?
struct ContentView: View {
#State var time: String = "";
var body: some View {
VStack {
Text("it is: \(time)")
Secondary(secondTime: $time)
Button("Change time") {
time = "2 poclock"
}
}
}
}
struct Secondary: View {
#Binding var secondTime: String;
var body: some View {
Text("secondary time is \(secondTime)")
Button("Change time again from Secondary View") {
secondTime = "3 oclock"
}
}
}
In Secondary use:
#State var secondTime: String
and in ContentView use:
Secondary(secondTime: time)
not $time
EDIT1:
If you want to click the button in ContentView to change both views,
but Secondary only changes itself, then try this approach:
struct Secondary: View {
#Binding var secondTime: String
#State var localTime: String = ""
var body: some View {
Text("Secondary time is \(localTime)") // <--- here
.onChange(of: secondTime) { newval in // <--- here
localTime = newval // <--- here
}
Button("Change time again from Secondary View") {
localTime = "3 oclock " + String(Int.random(in: 1..<100)) // <-- to show changes
}
}
}
struct ContentView: View {
#State var time: String = ""
var body: some View {
VStack (spacing: 55) {
Text("ContentView it is: \(time)")
Secondary(secondTime: $time)
Button("Change time") {
time = "2 oclock " + String(Int.random(in: 1..<100)) // <-- to show changes
}
}
}
}

How to correctly handle Picker in Update Views (SwiftUI)

I'm quite new to SwiftUI and I'm wondering how I should use a picker in an update view correctly.
At the moment I have a form and load the data in with .onAppear(). That works fine but when I try to pick something and go back to the update view the .onAppear() gets called again and I loose the picked value.
In the code it looks like this:
import SwiftUI
struct MaterialUpdateView: View {
// Bindings
#State var material: Material
// Form Values
#State var selectedUnit = ""
var body: some View {
VStack(){
List() {
Section(header: Text("MATERIAL")){
// Picker for the Unit
Picker(selection: $selectedUnit, label: Text("Einheit")) {
ForEach(API().units) { unit in
Text("\(unit.name)").tag(unit.name)
}
}
}
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Does anyone has experience with that problem or am I doing something terribly wrong?
You need to create a custom binding which we will implement in another subview. This subview will be initialised with the binding vars selectedUnit and material
First, make your MaterialUpdateView:
struct MaterialUpdateView: View {
// Bindings
#State var material : Material
// Form Values
#State var selectedUnit = ""
var body: some View {
NavigationView {
VStack(){
List() {
Section(header: Text("MATERIAL")) {
MaterialPickerView(selectedUnit: $selectedUnit, material: $material)
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Then, below, add your MaterialPickerView, as shown:
Disclaimer: You need to be able to access your API() from here, so move it or add it in this view. As I have seen that you are re-instanciating it everytime, maybe it is better that you store its instance with let api = API() and then refer to it with api, and even pass it to this view as such!
struct MaterialPickerView: View {
#Binding var selectedUnit: String
#Binding var material : Material
#State var idx: Int = 0
var body: some View {
let binding = Binding<Int>(
get: { self.idx },
set: {
self.idx = $0
self.selectedUnit = API().units[self.idx].name
self.material.unit = self.selectedUnit
})
return Picker(selection: binding, label: Text("Einheit")) {
ForEach(API().units.indices) { i in
Text(API().units[i].name).tag(API().units[i].name)
}
}
}
}
That should do,let me know if it works!

SwiftUI SceneDelegate - contentView Missing argument for parameter 'index' in call

I am trying to create a list using ForEach and NavigationLink of an array of data.
I believe my code (see the end of the post) is correct but my build fails due to
"Missing argument for parameter 'index' in call" and takes me to SceneDelegate.swift a place I haven't had to venture before.
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
I can get the code to run if I amend to;
let contentView = ContentView(habits: HabitsList(), index: 1)
but then all my links hold the same data, which makes sense since I am naming the index position.
I have tried, index: self.index (which is what I am using in my NavigationLink) and get a different error message - Cannot convert value of type '(Any) -> Int' to expected argument type 'Int'
Below are snippets of my code for reference;
struct HabitItem: Identifiable, Codable {
let id = UUID()
let name: String
let description: String
let amount: Int
}
class HabitsList: ObservableObject {
#Published var items = [HabitItem]()
}
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var index: Int
var body: some View {
NavigationView {
List {
ForEach(habits.items) { item in
NavigationLink(destination: HabitDetail(habits: self.habits, index: self.index)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}
}
}
}
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var habits: HabitsList
var index: Int
var body: some View {
NavigationView {
Form {
Text(self.habits.items[index].name)
}
}
}
}
You probably don't need to pass the whole ObservedObject to the HabitDetail.
Passing just a HabitItem should be enough:
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
let item: HabitItem
var body: some View {
// remove `NavigationView` form the detail view
Form {
Text(item.name)
}
}
}
Then you can modify your ContentView:
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var body: some View {
NavigationView {
List {
// for every item in habits create a `linkView`
ForEach(habits.items, id:\.id) { item in
self.linkView(item: item)
}
}
}
}
// extract to another function for clarity
func linkView(item: HabitItem) -> some View {
// pass just a `HabitItem` to the `HabitDetail`
NavigationLink(destination: HabitDetail(item: item)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}

Handling derived state in SwiftUI

say I am creating an "Date Editor" view. The goal is:
- Take a default, seed date.
- It lets the user alter the input.
- If the user then chooses, they can press "Save", in which case the owner of the view can decide to do something with the data.
Here's one way to implement it:
struct AlarmEditor : View {
var seedDate : Date
var handleSave : (Date) -> Void
#State var editingDate : Date?
var body : some View {
let dateBinding : Binding<Date> = Binding(
get: {
return self.editingDate ?? seedDate
},
set: { date in
self.editingDate = date
}
)
return VStack {
DatePicker(
selection: dateBinding,
displayedComponents: .hourAndMinute,
label: { Text("Date") }
)
Spacer()
Button(action: {
self.handleSave(dateBinding.wrappedValue)
}) {
Text("Save").font(.headline).bold()
}
}
}
}
The Problem
What if the owner changes the value of seedDate?
Say in that case, what I wanted to do was to reset the value of editingDate to the new seedDate.
What would be an idiomatic way of doing this?
I'm not sure that I have understand the purpose of the seedDate here. But I think you are relying on events (kind of UIKit way) a bit too much instead of the single source of truth principle (the SwiftUI way).
Update: Added a way to cancel the date edition.
In that case, the editor view should mutate the Binding only when saving. To do so, it uses a private State that will be used for the date picker. This way, the source of truth is preserved as the private state used will never leave the context of the editing view.
struct ContentView: View {
#State var dateEditorVisible = false
#State var date: Date = Date() // source of truth
var body: some View {
NavigationView {
VStack {
Text("\(date.format("HH:mm:ss"))")
Button(action: self.showDateEditor) {
Text("Edit")
}
.sheet(isPresented: $dateEditorVisible) {
// Here we provide a two way binding to the `date` state
// and a way to dismiss the editor view.
DateEditorView(date: self.$date, dismiss: self.hideDateEditor)
}
}
}
}
func showDateEditor() {
dateEditorVisible = true
}
func hideDateEditor() {
dateEditorVisible = false
}
}
struct DateEditorView: View {
// Only a binding.
// Updating this value will update the `#State date` of the parent view
#Binding var date: Date
#State private var editingDate: Date = Date()
private var dismiss: () -> Void
init(date: Binding<Date>, dismiss: #escaping () -> Void) {
self._date = date
self.dismiss = dismiss
// assign the wrapped value as default value for edition
self.editingDate = date.wrappedValue
}
var body: some View {
VStack {
DatePicker(selection: $editingDate, displayedComponents: .hourAndMinute) {
Text("Date")
}
HStack {
Button(action: self.save) {
Text("Save")
}
Button(action: self.dismiss) {
Text("Cancel")
}
}
}
}
func save() {
date = editingDate
dismiss()
}
}
With this way, you don't need to define a save action to update the parent view or keep in sync the current value with some default value. You only have a single source of truth that drives all of your UI.
Edit:
The Date extension to make it build.
extension Date {
private static let formater = DateFormatter()
func format(_ format: String) -> String {
Self.formater.dateFormat = format
return Self.formater.string(from: self)
}
}
I would prefer to do this via explicitly used ViewModel for such editor, and it requires minimal modifications in your code. Here is possible approach (tested & worked with Xcode 11.2.1):
Testing parent
struct TestAlarmEditor: View {
private var editorModel = AlarmEditorViewModel()
var body: some View {
VStack {
AlarmEditor(viewModel: self.editorModel, handleSave: {_ in }, editingDate: nil)
Button("Reset") {
self.editorModel.seedDate = Date(timeIntervalSinceNow: 60 * 60)
}
}
}
}
Simple view model for editor
class AlarmEditorViewModel: ObservableObject {
#Published var seedDate = Date() // << can be any or set via init
}
Updated editor
struct AlarmEditor : View {
#ObservedObject var viewModel : AlarmEditorViewModel
var handleSave : (Date) -> Void
#State var editingDate : Date?
var body : some View {
let dateBinding : Binding<Date> = Binding(
get: {
return self.editingDate ?? self.viewModel.seedDate
},
set: { date in
self.editingDate = date
}
)
return VStack {
DatePicker(
selection: dateBinding,
displayedComponents: .hourAndMinute,
label: { Text("Date") }
)
.onReceive(self.viewModel.$seedDate, perform: {
self.editingDate = $0 }) // << reset here
Spacer()
Button(action: {
self.handleSave(dateBinding.wrappedValue)
}) {
Text("Save").font(.headline).bold()
}
}
}
}
Comment and warning
Basically, this question amounts to looking for a replacement for didSet on the OP's var seedDate.
I used one of my support requests with Apple on this same question a few months ago. The latest response from them was that they have received several questions like this, but they don't have a "good" solution yet. I shared the solution below and they answered "Since it's working, use it."
What follows below is quite "smelly" but it does work. Hopefully we'll see improvements in iOS 14 that remove the necessity for something like this.
Concept
We can take advantage of the fact that body is the only entrance point for view rendering. Therefore, we can track changes to our view's inputs over time and change internal state based on that. We just have to be careful about how we update things so that SwiftUI's idea of State is not modified incorrectly.
We can do this by using a struct that contains two reference values:
The value we want to track
The value we want to modify when #1 changes
If we want SwiftUI to update we replace the reference value. If we want to update based on changes to #1 inside the body, we update the value held by the reference value.
Implementation
Gist here
First, we want to wrap any value in a reference type. This allows us to save a value without triggering SwiftUI's update mechanisms.
// A class that lets us wrap any value in a reference type
class ValueHolder<Value> {
init(_ value: Value) { self.value = value }
var value: Value
}
Now, if we declare #State var valueHolder = ValueHolder(0) we can do:
Button("Tap me") {
self.valueHolder.value = 0 // **Doesn't** trigger SwiftUI update
self.valueHolder = ValueHolder(0) // **Does** trigger SwiftUI update
}
Second, create a property wrapper that holds two of these, one for our external input value, and one for our internal state.
See this answer for an explanation of why I use State in the property wrapper.
// A property wrapper that holds a tracked value, and a value we'd like to update when that value changes.
#propertyWrapper
struct TrackedValue<Tracked, Value>: DynamicProperty {
var trackedHolder: State<ValueHolder<Tracked>>
var valueHolder: State<ValueHolder<Value>>
init(wrappedValue value: Value, tracked: Tracked) {
self.trackedHolder = State(initialValue: ValueHolder(tracked))
self.valueHolder = State(initialValue: ValueHolder(value))
}
var wrappedValue: Value {
get { self.valueHolder.wrappedValue.value }
nonmutating set { self.valueHolder.wrappedValue = ValueHolder(newValue) }
}
var projectedValue: Self { return self }
}
And finally add a convenience method to let us efficiently update when we need to. Since this returns a View you can use it inside of any ViewBuilder.
extension TrackedValue {
#discardableResult
public func update(tracked: Tracked, with block:(Tracked, Value) -> Value) -> some View {
self.valueHolder.wrappedValue.value = block(self.trackedHolder.wrappedValue.value, self.valueHolder.wrappedValue.value)
self.trackedHolder.wrappedValue.value = tracked
return EmptyView()
}
}
Usage
If you run the below code, childCount will reset to 0 every time masterCount changes.
struct ContentView: View {
#State var count: Int = 0
var body: some View {
VStack {
Button("Master Count: \(self.count)") {
self.count += 1
}
ChildView(masterCount: self.count)
}
}
}
struct ChildView: View {
var masterCount: Int
#TrackedValue(tracked: 0) var childCount: Int = 0
var body: some View {
self.$childCount.update(tracked: self.masterCount) { (old, myCount) -> Int in
if self.masterCount != old {
return 0
}
return myCount
}
return Button("Child Count: \(self.childCount)") {
self.childCount += 1
}
}
}
following your code, I would do something like this.
struct AlarmEditor: View {
var handleSave : (Date) -> Void
#State var editingDate : Date
init(seedDate: Date, handleSave: #escaping (Date) -> Void) {
self._editingDate = State(initialValue: seedDate)
self.handleSave = handleSave
}
var body: some View {
Form {
DatePicker(
selection: $editingDate,
displayedComponents: .hourAndMinute,
label: { Text("Date") }
)
Spacer()
Button(action: {
self.handleSave(self.editingDate)
}) {
Text("Save").font(.headline).bold()
}
}
}//body
}//AlarmEditor
struct AlarmEditor_Previews: PreviewProvider {
static var previews: some View {
AlarmEditor(seedDate: Date()) { editingDate in
print(editingDate.description)
}
}
}
And, use it like this elsewhere.
AlarmEditor(seedDate: Date()) { editingDate in
//do anything you want with editingDate
print(editingDate.description)
}
this is my sample output:
2020-02-07 23:39:42 +0000
2020-02-07 22:39:42 +0000
2020-02-07 23:39:42 +0000
2020-02-07 21:39:42 +0000
what do you think? 50 points

Passing filtered #Bindable objects to multiple views in SwiftUI

I’m trying to pass a filter array to multiple views, but the filtering is not working. If I remove the filter, you can pass the array to the next view, but that leads to another error during the ForEach loop. I've posted all the code below.
Does anyone know how you can pass a filter version of a #Bindable array? Also why can't I print sport.name and sport.isFavorite.description in the ForEach loop?
I’m using swiftUI on Xcode 11.0 beta 5.
import SwiftUI
import Combine
struct Sport: Identifiable{
var id = UUID()
var name : String
var isFavorite = false
}
final class SportData: ObservableObject {
#Published var store =
[
Sport(name: "soccer", isFavorite: false),
Sport(name: "tennis", isFavorite: false),
Sport(name: "swimming", isFavorite: true),
Sport(name: "running", isFavorite: true)
]
}
struct Testing: View {
#ObservedObject var sports = SportData()
var body: some View {
VStack {
TestingTwo(sports: $sports.store.filter({$0.isFavorite}))
}
}
}
struct TestingTwo: View {
#Binding var sports : [Sport]
var body: some View {t
NavigationView {
VStack(spacing: 10){
ForEach($sports) { sport in
NavigationLink(destination: TestingThree(sport: sport)){
HStack {
Text(sport.name)
Spacer()
Text(sport.isFavorite.description)
}
.padding(.horizontal)
.frame(width: 200, height: 50)
.background(Color.blue)
}
}
}
}
}
}
struct TestingThree: View {
#Binding var sport : Sport
var body: some View {
VStack {
Text(sport.isFavorite.description)
.onTapGesture {
self.sport.isFavorite.toggle()
}
}
}
}
#if DEBUG
struct Testing_Previews: PreviewProvider {
static var previews: some View {
Testing()
}
}
#endif
Filtering in your case might be better placed in the navigation view, due to your binding requirements.
struct Testing: View {
#ObservedObject var sports = SportData()
var body: some View {
VStack {
TestingTwo(sports: $sports.store)
}
}
}
struct TestingTwo: View {
#Binding var sports : [Sport]
#State var onlyFavorites = false
var body: some View {t
NavigationView {
VStack(spacing: 10){
ForEach($sports) { sport in
if !self.onlyFavorites || sport.value.isFavorite {
NavigationLink(destination: TestingThree(sport: sport)){
HStack {
Text(sport.value.name)
Spacer()
Text(sport.value.isFavorite.description)
}
.padding(.horizontal)
.frame(width: 200, height: 50)
.background(Color.blue)
}
}
}
}
}
}
}
Now you can switch the isFavorite state either within the action implementation of a button, or while specifying the integration of you TestingTwo view.
struct Testing: View {
#ObservedObject var sports = SportData()
var body: some View {
VStack {
TestingTwo(sports: $sports.store, onlyFavorites: true)
}
}
}
Regarding the second part of your question: Note the value addendum in the ForEach loop. You're dealing with as binding here (as ForEach($sports) indicates), hence sport is not an instance of Sport.
You can't get a #Binding from a computed property, since the computed property is computed dynamically. A typical way to avoid this is to pass in ids of the sports objects and the data store itself, whereby you can access the sports items via id from the store.
If you really want to pass a #Binding in you have to remove the filter (pass in an actually backed array) and modfy the ForEach like the following:
ForEach($sports.store) { (sport: Binding<Sport>) in