How to set up Binding of computed value inside class (SwiftUI) - swift

In my Model I have an array of Items, and a computed property proxy which using set{} and get{} to set and return currently selected item inside array and works as shortcut. Setting item's value manually as model.proxy?.value = 10 works, but can't figure out how to Bind this value to a component using $.
import SwiftUI
struct Item {
var value: Double
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(value: 1), Item(value: 2), Item(value: 3)]
var proxy: Item? {
get {
return items[1]
}
set {
items[1] = newValue!
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
Text("Value: \(model.proxy!.value)")
Button(action: {model.proxy?.value = 123}, label: {Text("123")}) // method 1: this works fine
SubView(value: $model.proxy.value) // method 2: binding won't work
}.padding()
}
}
struct SubView <B:BinaryFloatingPoint> : View {
#Binding var value: B
var body: some View {
Button( action: {value = 100}, label: {Text("1")})
}
}
Is there a way to modify proxy so it would be modifiable and bindable so both methods would be available?
Thanks!
Day 2: Binding
Thanks to George, I have managed to set up Binding, but the desired binding with SubView still won't work. Here is the code:
import SwiftUI
struct Item {
var value: Double
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(value: 0), Item(value: 0), Item(value: 0)]
var proxy: Binding <Item?> {
Binding <Item?> (
get: { self.items[1] },
set: { self.items[1] = $0! }
)
}
}
struct ContentView: View {
#StateObject var model = Model()
#State var myval: Double = 10
var body: some View {
VStack {
Text("Value: \(model.proxy.wrappedValue!.value)")
Button(action: {model.proxy.wrappedValue?.value = 555}, label: {Text("555")})
SubView(value: model.proxy.value) // this still wont work
}.padding()
}
}
struct SubView <T:BinaryFloatingPoint> : View {
#Binding var value: T
var body: some View {
Button( action: {value = 100}, label: {Text("B 100")})
}
}

Create a Binding instead.
Example:
var proxy: Binding<Item?> {
Binding<Item?>(
get: { items[1] },
set: { items[1] = $0! }
)
}

Related

SwiftUI: Pass an ObservableObject's property into another class's initializer

How do I pass a property from an ObservedObject in a View, to another class's initializer in the same View? I get an error with my ObservedObject:
Cannot use instance member 'project' within property initializer; property initializers run before 'self' is available
The reason I want to do this is I have a class which has properties that depend on a value from the ObservedObject.
For example, I have an ObservedObject called project. I want to use the property, project.totalWordsWritten, to change the session class's property, session.totalWordCountWithSession:
struct SessionView: View {
#Binding var isPresented: Bool
#ObservedObject var project: Project
// How to pass in project.totalWordsWritten from ObservedObject project to totalWordCount?
#StateObject var session:Session = Session(startDate: Date(), sessionWordCount: 300, totalWordCount: 4000)
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Form {
Section {
Text("Count")
HStack {
Text("Session word count")
TextField("", value: $session.sessionWordCount, formatter: NumberFormatter())
.textFieldStyle(.roundedBorder)
}
HStack {
// Changing text field here should change the session count above
Text("Total word count")
TextField("", value: $session.totalWordCountWithSession, formatter: NumberFormatter())
.textFieldStyle(.roundedBorder)
}
}
}
}.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// Save this session into the project
project.addSession(newSession: session)
isPresented = false
}
}
}
}
}
}
struct SessionView_Previews: PreviewProvider {
static var previews: some View {
SessionView(isPresented: .constant(true), project: Project(title: "TestProject", startWordCount: 0))
}
}
Below is the rest of the example:
HomeView.swift
import SwiftUI
struct HomeView: View {
#State private var showingSessionPopover:Bool = false
#StateObject var projectItem:Project = Project(title: "Test Project", startWordCount: 4000)
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text(projectItem.title).font(Font.custom("OpenSans-Regular", size: 18))
.fontWeight(.bold)
Text("Count today: \(projectItem.wordsWrittenToday)")
Text("Total: \(projectItem.totalWordsWritten)")
}
.toolbar {
ToolbarItem {
Button(action: {
showingSessionPopover = true
}, label: {
Image(systemName: "calendar").imageScale(.large)
}
)
}
}
}.popover(isPresented: $showingSessionPopover) {
SessionView(isPresented: $showingSessionPopover, project: projectItem)
}
}
}
Session.swift:
import Foundation
import SwiftUI
class Session: Identifiable, ObservableObject {
init(startDate:Date, sessionWordCount:Int, totalWordCount: Int) {
self.startDate = startDate
self.endDate = Calendar.current.date(byAdding: .minute, value: 30, to: startDate) ?? Date()
self.sessionWordCount = sessionWordCount
self.totalWordCount = totalWordCount
self.totalWordCountWithSession = self.totalWordCount + sessionWordCount
}
var id: UUID = UUID()
#Published var startDate:Date
#Published var endDate:Date
var totalWordCount: Int
var sessionWordCount:Int
#Published var totalWordCountWithSession:Int {
didSet {
sessionWordCount = totalWordCountWithSession - totalWordCount
}
}
}
Project.swift
import SwiftUI
class Project: Identifiable, ObservableObject {
var id: UUID = UUID()
#Published var title:String
var sessions:[Session] = []
#Published var wordsWrittenToday:Int = 0
#Published var totalWordsWritten:Int = 0
#Published var startWordCount:Int
init(title:String,startWordCount:Int) {
self.title = title
self.startWordCount = startWordCount
self.calculateDailyAndTotalWritten()
}
// Create a new session
func addSession(newSession:Session) {
sessions.append(newSession)
calculateDailyAndTotalWritten()
}
// Re-calculate how many
// today and in total for the project
func calculateDailyAndTotalWritten() {
wordsWrittenToday = 0
totalWordsWritten = startWordCount
for session in sessions {
if (Calendar.current.isDateInToday(session.startDate)) {
wordsWrittenToday += session.sessionWordCount
}
totalWordsWritten += session.sessionWordCount
}
}
}
You can use the StateObject initializer in init:
struct SessionView: View {
#Binding var isPresented: Bool
#ObservedObject var project: Project
#StateObject var session:Session = Session(startDate: Date(), sessionWordCount: 300, totalWordCount: 4000)
init(isPresented: Binding<Bool>, project: Project, session: Session) {
_isPresented = isPresented
_session = StateObject(wrappedValue: Session(startDate: Date(), sessionWordCount: 300, totalWordCount: project.totalWordsWritten))
self.project = project
}
var body: some View {
Text("Hello, world")
}
}
Note that the documentation says:
You don’t call this initializer directly
But, it has been confirmed by SwiftUI engineers in WWDC labs that this is a legitimate technique. What runs in wrappedValue is an autoclosure and only runs on the first init of StateObject, so you don't have to be concerned that every time your View updates that it will run.
In general, though, it's a good idea to try to avoid doing things in the View's init. You could consider instead, for example, using something like task or onAppear to set the value and just put a placeholder value in at first.

Passing a state variable to parent view

I have the following code:
struct BookView: View {
#State var title = ""
#State var author = ""
var body: some View {
TextField("Title", text: $title)
TextField("Author", text: $author)
}
}
struct MainView: View {
#State private var presentNewBook: Bool = false
var body: some View {
NavigationView {
// ... some button that toggles presentNewBook
}.sheet(isPresented: $presentNewBook) {
let view = BookView()
view.toolbar {
ToolbarItem(placement: principal) {
TextField("Title", text: view.$title)
}
}
}
}
}
This compiles but is giving me the following error on runtime:
Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
How do I pass a state variable to some other outside view? I can't use ObservableObject on BookView since that would require me to change it from struct to class
In general, your state should always be owned higher up the view hierarchy. Trying to access the child state from a parent is an anti-pattern.
One option is to use #Bindings to pass the values down to child views:
struct BookView: View {
#Binding var title : String
#Binding var author : String
var body: some View {
TextField("Title", text: $title)
TextField("Author", text: $author)
}
}
struct ContentView: View {
#State private var presentNewBook: Bool = false
#State private var title = ""
#State private var author = ""
var body: some View {
NavigationView {
VStack {
Text("Title: \(title)")
Text("Author: \(author)")
Button("Open") {
presentNewBook = true
}
}
}.sheet(isPresented: $presentNewBook) {
BookView(title: $title, author: $author)
}
}
}
Another possibility is using an ObservableObject:
class BookState : ObservableObject {
#Published var title = ""
#Published var author = ""
}
struct BookView: View {
#ObservedObject var bookState : BookState
var body: some View {
TextField("Title", text: $bookState.title)
TextField("Author", text: $bookState.author)
}
}
struct ContentView: View {
#State private var presentNewBook: Bool = false
#StateObject private var bookState = BookState()
var body: some View {
NavigationView {
VStack {
Text("Title: \(bookState.title)")
Text("Author: \(bookState.author)")
Button("Open") {
presentNewBook = true
}
}
}.sheet(isPresented: $presentNewBook) {
BookView(bookState: bookState)
}
}
}
I've altered your example views a bit because to me the structure was unclear, but the concept of owning the state at the parent level is the important element.
You can also pass a state variable among views as such:
let view = BookView(title: "foobar")
view.toolbar {
ToolbarItem(placement: principal) {
TextField("Title", text: view.$title)
}
}
Then, inside of BookView:
#State var title: String
init(title: String) {
_title = State(initialValue: title)
}
Source: How could I initialize the #State variable in the init function in SwiftUI?

How to make shortcut/binding to a struct inside class? (SwiftUI)

In my class, I have an array of Item and an optional var selection, which is supposed to store a SHORTCUT to the selected Item.
I need to be able to access the selected Item by referring to selection.
In order for selection to work as SHORTCUT does selection has to be a Binding?
If yes, is it a #Binding like in structs, or maybe Binding<T>?
And does it has to be #Published?
My code:
import SwiftUI
struct Item: Identifiable, Equatable {
var id = UUID().uuidString
var color: Color
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(color: .blue), Item(color: .blue), Item(color: .blue)]
#Published var selection: Item? //this supposed to be not a value, but a SHORTCUT to a selected item inside array
func setSelection (item: Item) {
selection = item
}
func changeColor (color: Color) {
if selection != nil {
selection?.color = color// << PROBLEM is that it only copies object and modifies the copy instead of original
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
//list
VStack {
ForEach(model.items.indices, id:\.hashValue) { i in
SubView(item: $model.items[i], model: model)
}
// change color button
Button {
model.changeColor(color: .red)
} label: {Text("Make Selection Red")}
}.padding()
}
}
struct SubView: View {
#Binding var item: Item
var model: Model
var body: some View {
VStack {
// button which sets selection to an items inside this subview
Button {
model.setSelection(item: item)
} label: {
Text("Select").background(item.color)}.buttonStyle(PlainButtonStyle())
}
}
}
Desired functionality: click on one if items, and then charging its color.
since you want selection to be "....a selected item inside array", then you could
just use the index in the array of items. Something like this:
(although your code logic is a bit strange to me, I assumed this is just a test example)
struct Item: Identifiable, Equatable {
var id = UUID().uuidString
var color: Color
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(color: .blue), Item(color: .blue), Item(color: .blue)]
#Published var selection: Int? // <-- here
func changeColor(color: Color) {
if let ndx = selection { // <-- here
items[ndx].color = color
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
//list
VStack {
ForEach(model.items.indices, id:\.self) { i in
SubView(index: i, model: model) // <-- here
}
// change color button
Button {
model.changeColor(color: .red)
} label: {Text("Make Selection Red")}
}.padding()
}
}
struct SubView: View {
var index: Int // <-- here
#ObservedObject var model: Model // <-- here
var body: some View {
VStack {
// button which sets selection to an items inside this subview
Button {
model.selection = index
} label: {
Text("Select").background(model.items[index].color) // <-- here
}
.buttonStyle(PlainButtonStyle())
}
}
}

How do I validate dynamically added textFields on a button click in SwiftUI?

I have the following InputView struct and add those InputViews dynamically within a foreach loop in another view:
struct InputView: View {
#State private var input: String = ""
var correct_input: Int
var body: some View {
TextField("?", text: $input)
.foregroundColor(setColor())
}
func setColor() -> Color {
if (Int(input) == correct_input) {
return Color.green
}
return Color.red
}
}
Up to now it is shown immediately whether the input is correct. However, I would like to add a button so that the input of all InputViews is only validated when it is clicked. How can I achieve this in SwiftUI?
You can be done this by making a model of text fields and use one isValid flag for each InputView for the track.
Here, is the possible demo solution.
struct TextFieldModel: Identifiable {
var id = UUID()
var input: String
var correctInput: Int
var isValidate: Bool = true
}
struct InputView: View {
#Binding var input: TextFieldModel
var body: some View {
TextField("?", text: $input.input)
.foregroundColor(input.isValidate ? Color.blue : Color.red)
}
}
struct ContentViewTextFields: View {
#State var arrTextFields: [TextFieldModel] = [
.init(input: "", correctInput: 5),
.init(input: "", correctInput: 10),
.init(input: "", correctInput: 1)
]
#State var isValidate: Bool = true
var body: some View {
VStack{
ForEach(arrTextFields.indices) { index in
InputView(input: $arrTextFields[index])
.background(Color.gray.opacity(0.2))
.padding()
}
Spacer()
Button("Validate") {
// Here validate all text
arrTextFields.indices.forEach({arrTextFields[$0].isValidate = (Int(arrTextFields[$0].input) == arrTextFields[$0].correctInput) })
}
}
}
}
You can have a button to check the input, setting some #State variable like correct to true if it is correct.
Example:
struct ContentView: View {
var body: some View {
InputView(correctInput: 5)
}
}
struct InputView: View {
#State private var input = ""
#State private var correct = false
let correctInput: Int
var body: some View {
VStack {
TextField("?", text: $input)
.foregroundColor(correct ? .green : .red)
Button("Check answer") {
correct = Int(input) == correctInput
}
}
}
}

Use object property as binding variable [duplicate]

I have the following InputView struct and add those InputViews dynamically within a foreach loop in another view:
struct InputView: View {
#State private var input: String = ""
var correct_input: Int
var body: some View {
TextField("?", text: $input)
.foregroundColor(setColor())
}
func setColor() -> Color {
if (Int(input) == correct_input) {
return Color.green
}
return Color.red
}
}
Up to now it is shown immediately whether the input is correct. However, I would like to add a button so that the input of all InputViews is only validated when it is clicked. How can I achieve this in SwiftUI?
You can be done this by making a model of text fields and use one isValid flag for each InputView for the track.
Here, is the possible demo solution.
struct TextFieldModel: Identifiable {
var id = UUID()
var input: String
var correctInput: Int
var isValidate: Bool = true
}
struct InputView: View {
#Binding var input: TextFieldModel
var body: some View {
TextField("?", text: $input.input)
.foregroundColor(input.isValidate ? Color.blue : Color.red)
}
}
struct ContentViewTextFields: View {
#State var arrTextFields: [TextFieldModel] = [
.init(input: "", correctInput: 5),
.init(input: "", correctInput: 10),
.init(input: "", correctInput: 1)
]
#State var isValidate: Bool = true
var body: some View {
VStack{
ForEach(arrTextFields.indices) { index in
InputView(input: $arrTextFields[index])
.background(Color.gray.opacity(0.2))
.padding()
}
Spacer()
Button("Validate") {
// Here validate all text
arrTextFields.indices.forEach({arrTextFields[$0].isValidate = (Int(arrTextFields[$0].input) == arrTextFields[$0].correctInput) })
}
}
}
}
You can have a button to check the input, setting some #State variable like correct to true if it is correct.
Example:
struct ContentView: View {
var body: some View {
InputView(correctInput: 5)
}
}
struct InputView: View {
#State private var input = ""
#State private var correct = false
let correctInput: Int
var body: some View {
VStack {
TextField("?", text: $input)
.foregroundColor(correct ? .green : .red)
Button("Check answer") {
correct = Int(input) == correctInput
}
}
}
}