How would I create a custom binding from a computed property? - swiftui

So I know that I can create a custom binding in the view itself (in the body) But I'm wondering if I can just have a computed property return me a Binding .
I am using generics to handle whatever type I pass in. And then show that generic type in a Text and of course to do that it needs to be a string.
So here is what I am trying to do.
struct EditStatView<Stat>: View {
#Binding var statValue: Stat
let statLabel: String
var stringText: Binding<String> {
let stringValue = String(describing: statValue)
return Binding<String>(stringValue) //<- Cannot convert value of type 'String' to expected argument type 'Binding<String?>'
}
}
Confused about how to continue

import SwiftUI
struct EditStatParentView: View {
#State var sample1: String = "init"
#State var sample2: Int = 0
#State var sample3: Double = 0.0
var body: some View {
VStack{
Text(sample1)
EditStatView(statValue: $sample1, statLabel: "string")
Divider()
Text(sample2.description)
EditStatView(statValue: $sample2, statLabel: "int")
Divider()
Text(sample3.description)
EditStatView(statValue: $sample3, statLabel: "double")
}
}
}
struct EditStatView<Stat>: View where Stat: Any{
#Binding var statValue: Stat
let statLabel: String
//The point of a Binding is a two-way connection
//It needs a parent that is a source of truth such as
//#State, #Published, ManagedObject
var statProxy: Binding<String>{
Binding(get: {
return String(describing: statValue)
}, set: {
// because of the two-way connection you need to make sure $0 matches the type of the original Stat or it will fail.
//Here is a very crude way to make it adapt
statValue = $0 as? Stat ?? Double($0) as? Stat ?? Int($0) as? Stat ?? statValue //If all else fails just return the original
})
}
var body: some View {
VStack{
Text(String(describing: statValue))
TextField(statLabel, text: statProxy)
}
}
}
struct EditStatView_Previews: PreviewProvider {
static var previews: some View {
EditStatParentView()
}
}

Related

SwiftUI - Binding in ObservableObject

Let's say we have a parent view like:
struct ParentView: View {
#State var text: String = ""
var body: some View {
ChildView(text: $text)
}
}
Child view like:
struct ChildView: View {
#ObservedObject var childViewModel: ChildViewModel
init(text: Binding<String>) {
self.childViewModel = ChildViewModel(text: text)
}
var body: some View {
...
}
}
And a view model for the child view:
class ChildViewModel: ObservableObject {
#Published var value = false
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
...
}
Making changes on the String binding inside the child's view model makes the ChildView re-draw causing the viewModel to recreate itself and hence reset the #Published parameter to its default value. What is the best way to handle this in your opinion?
Cheers!
The best way is to use a custom struct as a single source of truth, and pass a binding into child views, e.g.
struct ChildViewConfig {
var value = false
var text: String = ""
// mutating funcs for logic
mutating func reset() {
text = ""
}
}
struct ParentView: View {
#State var config = ChildViewConfig()
var body: some View {
ChildView(config: $config)
}
}
struct ChildView: View {
#Binding var config: ChildViewConfig
var body: some View {
TextField("Text", text: $config.text)
...
Button("Reset") {
config.reset()
}
}
}
"ViewConfig can maintain invariants on its properties and be tested independently. And because ViewConfig is a value type, any change to a property of ViewConfig, like its text, is visible as a change to ViewConfig itself." [Data Essentials in SwiftUI WWDC 2020].

How To Assign TextField Value To Environment Object -- Error Cannot convert value of type 'String' to expected argument type 'Binding<String>"

I try to assign a text field value to a EnvironmentObject, but getting this error.
Cannot convert value of type 'String' to expected argument type 'Binding"
import SwiftUI
class FilterSelections: ObservableObject {
#Published var filters: [String: Any] = [
"testFilter": "na"]
#Published var fromDate: Date = Date()
#Published var testNumber: String = ""
}
struct MainView: View {
var body: some View {
ZStack {
ScrollView {
VStack{
NumberSearchSubView()
Divider()
EmptyView()
}
}
}
}
}
struct NumberSearchSubView: View {
#State var searchByNumber: String = ""
#EnvironmentObject var filters: FilterSelections
var body: some View {
VStack(alignment:.leading, spacing:10) {
TextField("Enter your number", text: filters.testNumber).padding(10)
}
}
}
In order to use a #Published property as a Binding, you have to use the $ symbol as a prefix:
TextField("Enter your number", text: $filters.testNumber)
This sends the projected value (ie the Binding) instead of just the String value.

#State var doesn't store value

My goal is to pass values between views, from Chooser to ThemeEditor. When the user presses an icon, I'm saving the object that I want to pass and later, using sheet and passing the newly created view with the content of the #State var.
The assignment is done successfully to the #State var themeToEdit, however it is nil when the ThemeEditor view is created in the sheet
What am I doing wrong?
struct Chooser: View {
#EnvironmentObject var store: Store
#State private var showThemeEditor = false
#State private var themeToEdit: ThemeContent?
#State private var editMode: EditMode = .inactive
#State private var isValid = false
var body: some View {
NavigationView {
List {
ForEach(self.store.games) { game in
NavigationLink(destination: gameView(game))
{
Image(systemName: "wrench.fill")
.imageScale(.large)
.opacity(editMode.isEditing ? 1 : 0)
.onTapGesture {
self.showThemeEditor = true
/* themeInfo is of type struct ThemeContent: Codable, Hashable, Identifiable */
self.themeToEdit = game.themeInfo
}
VStack (alignment: .leading) {
Text(self.store.name(for: something))
HStack{
/* some stuff */
Text(" of: ")
Text("Interesting info")
}
}
}
}
.sheet(isPresented: $showThemeEditor) {
if self.themeToEdit != nil { /* << themeToEdit is nil here - always */
ThemeEditor(forTheme: self.themeToEdit!, $isValid)
}
}
}
.environment(\.editMode, $editMode)
}
}
}
struct ThemeEditor: View {
#State private var newTheme: ThemeContent
#Binding var isValid: Bool
#State private var themeName = ""
init(forTheme theme: ThemeContent, isValid: Binding<Bool>) {
self._newTheme = State(wrappedValue: theme)
self._validThemeEdited = isValid
}
var body: some View {
....
}
}
struct ThemeContent: Codable, Hashable, Identifiable {
/* stores simple typed variables of information */
}
The .sheet content view is captured at the moment of creation, so if you want to check something inside, you need to use .sheet(item:) variant, like
.sheet(item: self.$themeToEdit) { item in
if item != nil {
ThemeEditor(forTheme: item!, $isValid)
}
}
Note: it is not clear what is ThemeContent, but it might be needed to conform it to additional protocols.
Use Binding. Change your ThemeEditor view with this.
struct ThemeEditor: View {
#Binding private var newTheme: ThemeContent?
#Binding var isValid: Bool
#State private var themeName = ""
init(forTheme theme: Binding<ThemeContent?>, isValid: Binding<Bool>) {
self._newTheme = theme
self._isValid = isValid
}
var body: some View {
....
}
}
And for sheet code
.sheet(isPresented: $showThemeEditor) {
ThemeEditor(forTheme: $themeToEdit, isValid: $isValid)
}
On Action
.onTapGesture {
/* themeInfo is of type struct ThemeContent: Codable, Hashable, Identifiable */
self.themeToEdit = game.themeInfo
self.showThemeEditor = true
}

How we can read #State variable from other struct in SwiftUI?

I want to know if there is simple or proper way to read a State variable value from a different View, I know the usage of .onChange or Binding or ObservableObject(class) and ..., but I like to know is there any other better way?
For example in this code I have a View called TextView which has a State value, and I am calling this View inside my ContentView, Now I put a Text in my ContentView which I want to read the State Value of TextView. Is there a spacial method for this job?
struct ContentView: View {
#State var readStringOfTextView: String = ""
var body: some View {
TextView()
Text(readStringOfTextView)
.foregroundColor(Color.blue)
}
}
struct TextView: View {
#State var stringOfText: String = "Hello, world!"
var body: some View {
Text(stringOfText)
.padding()
.foregroundColor(Color.red)
}
}
The entire point of State is that it's internal to a View. If you're trying to read it elsewhere, something has gone wrong in your design. The tool you want in this case is #Binding. ContentView should pass a Binding to TextView. Any changes in TextView will be seen by ContentView (in your example, this doesn't make sense, because stringOfText can't change, but I assume that the rest of your code changes it somehow). In your example, that would look something like this:
struct ContentView: View {
#State var readStringOfTextView: String = ""
var body: some View {
TextView(stringOfText: $readStringOfTextView)
Text(readStringOfTextView)
.foregroundColor(Color.blue)
}
}
struct TextView: View {
#Binding var stringOfText : String
var body: some View {
Text(stringOfText)
.padding()
.foregroundColor(Color.red)
.onAppear {
stringOfText = "Hello, world!"
}
}
}
In is possible to directly pass data up the view hierarchy using Preferences, but it's much more complicated, and not the right tool for the problem you've described. Even so, this is what it would look like:
Create a PreferenceKey to pass the data
Set the PreferenceKey in the child view(s) using .preference
Read the PreferenceKey in the parent view using .onPreferenceChange or .overlayPreferenceValue or .backgroundPreferenceValue.
struct TextPreference: PreferenceKey {
static var defaultValue = "default"
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
struct ContentView: View {
#State var readStringOfTextView: String = ""
var body: some View {
VStack {
TextView()
.onPreferenceChange(TextPreference.self) { value in
readStringOfTextView = value
}
Text(readStringOfTextView)
.foregroundColor(Color.blue)
}
}
}
struct TextView: View {
#State var stringOfText : String = "Hello, world!"
var body: some View {
Text(stringOfText)
.padding()
.foregroundColor(Color.red)
.preference(key: TextPreference.self, value: stringOfText)
}
}

Passing data between two views

I wanted to create quiet a simple app on watchOS 6, but after Apple has changed the ObjectBindig in Xcode 11 beta 5 my App does not run anymore. I simply want to synchronize data between two Views.
So I have rewritten my App with the new #Published, but I can't really set it up:
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen()) {
Text("Next View")
}
}
}
}
struct secondScreen: View {
#ObservedObject var input = UserInput()
var body: some View {
VStack {
Text("Button has been pushed \(input.score)")
Button(action: {self.input.score += 1
}) {
Text("Adder")
}
}
}
}
Your code has a couple of errors:
1) You didn't put your ContentView in a NavigationView, so the navigation between the two views never happened.
2) You used data binding in a wrong way. If you need the second view to rely on some state belonging to the first view you need to pass a binding to that state to the second view. Both in your first view and in your second view you had an #ObservedObject created inline:
#ObservedObject var input = UserInput()
so, the first view and the second one worked with two totally different objects. Instead, you are interested in sharing the score between the views. Let the first view own the UserInput object and just pass a binding to the score integer to the second view. This way both the views will work on the same value (you can copy paste the code below and try yourself).
import SwiftUI
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen(score: self.$input.score)) {
Text("Next View")
}
}
}
}
}
struct secondScreen: View {
#Binding var score: Int
var body: some View {
VStack {
Text("Button has been pushed \(score)")
Button(action: {self.score += 1
}) {
Text("Adder")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
If you really need it you can even pass the entire UserInput object to the second view:
import SwiftUI
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput() //please, note the difference between this...
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen(input: self.input)) {
Text("Next View")
}
}
}
}
}
struct secondScreen: View {
#ObservedObject var input: UserInput //... and this!
var body: some View {
VStack {
Text("Button has been pushed \(input.score)")
Button(action: {self.input.score += 1
}) {
Text("Adder")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
I tried a lot of different approaches on how to pass data from one view to another and came up with a solution that fits for simple and complex views / view models.
Version
Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)
This solution works with iOS 14.0 upwards, because you need the .onChange() view modifier. The example is written in Swift Playgrounds. If you need an onChange like modifier for lower versions, you should write your own modifier.
Main View
The main view has a #StateObject viewModel handling all of the views logic, like the button tap and the "data" (testingID: String) -> Check the ViewModel
struct TestMainView: View {
#StateObject var viewModel: ViewModel = .init()
var body: some View {
VStack {
Button(action: { self.viewModel.didTapButton() }) {
Text("TAP")
}
Spacer()
SubView(text: $viewModel.testingID)
}.frame(width: 300, height: 400)
}
}
Main View Model (ViewModel)
The viewModel publishes a testID: String?. This testID can be any kind of object (e.g. configuration object a.s.o, you name it), for this example it is just a string also needed in the sub view.
final class ViewModel: ObservableObject {
#Published var testingID: String?
func didTapButton() {
self.testingID = UUID().uuidString
}
}
So by tapping the button, our ViewModel will update the testID. We also want this testID in our SubView and if it changes, we also want our SubView to recognize and handle these changes. Through the ViewModel #Published var testingID we are able to publish changes to our view. Now let's take a look at our SubView and SubViewModel.
SubView
So the SubView has its own #StateObject to handle its own logic. It is completely separated from other views and ViewModels. In this example the SubView only presents the testID from its MainView. But remember, it can be any kind of object like presets and configurations for a database request.
struct SubView: View {
#StateObject var viewModel: SubviewModel = .init()
#Binding var test: String?
init(text: Binding<String?>) {
self._test = text
}
var body: some View {
Text(self.viewModel.subViewText ?? "no text")
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
.onAppear(perform: { self.viewModel.updateText(text: test) })
}
}
To "connect" our testingID published by our MainViewModel we initialize our SubView with a #Binding. So now we have the same testingID in our SubView. But we don't want to use it in the view directly, instead we need to pass the data into our SubViewModel, remember our SubViewModel is a #StateObject to handle all the logic. And we can't pass the value into our #StateObject during view initialization. Also if the data (testingID: String) changes in our MainViewModel, our SubViewModel should recognize and handle these changes.
Therefore we are using two ViewModifiers.
onChange
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
The onChange modifier subscribes to changes in our #Binding property. So if it changes, these changes get passed to our SubViewModel. Note that your property needs to be Equatable. If you pass a more complex object, like a Struct, make sure to implement this protocol in your Struct.
onAppear
We need onAppear to handle the "first initial data" because onChange doesn't fire the first time your view gets initialized. It is only for changes.
.onAppear(perform: { self.viewModel.updateText(text: test) })
Ok and here is the SubViewModel, nothing more to explain to this one I guess.
class SubviewModel: ObservableObject {
#Published var subViewText: String?
func updateText(text: String?) {
self.subViewText = text
}
}
Now your data is in sync between your MainViewModel and SubViewModel and this approach works for large views with many subviews and subviews of these subviews and so on. It also keeps your views and corresponding viewModels enclosed with high reusability.
Working Example
Playground on GitHub:
https://github.com/luca251117/PassingDataBetweenViewModels
Additional Notes
Why I use onAppear and onChange instead of only onReceive: It appears that replacing these two modifiers with onReceive leads to a continuous data stream firing the SubViewModel updateText multiple times. If you need to stream data for presentation, it could be fine but if you want to handle network calls for example, this can lead to problems. That's why I prefer the "two modifier approach".
Personal Note: Please don't modify the StateObject outside the corresponding view's scope. Even if it is somehow possible, it is not what its meant for.
My question is still related to how to pass data between two views but I have a more complicated JSON data set and I am running into problems both with the passing the data and with it's initialization. I have something that works but I am sure it is not correct. Here is the code. Help!!!!
/ File: simpleContentView.swift
import SwiftUI
// Following is the more complicated #ObservedObject (Buddy and class Buddies)
struct Buddy : Codable, Identifiable, Hashable {
var id = UUID()
var TheirNames: TheirNames
var dob: String = ""
var school: String = ""
enum CodingKeys1: String, CodingKey {
case id = "id"
case Names = "Names"
case dob = "dob"
case school = "school"
}
}
struct TheirNames : Codable, Identifiable, Hashable {
var id = UUID()
var first: String = ""
var middle: String = ""
var last: String = ""
enum CodingKeys2: String, CodingKey {
case id = "id"
case first = "first"
case last = "last"
}
}
class Buddies: ObservableObject {
#Published var items: [Buddy] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {UserDefaults.standard.set(encoded, forKey: "Items")}
}
}
#Published var buddy: Buddy
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([Buddy].self, from: items) {
self.items = decoded
// ??? How to initialize here
self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
return
}
}
// ??? How to initialize here
self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
self.items = []
}
}
struct simpleContentView: View {
#Environment(\.presentationMode) var presentationMode
#State private var showingSheet = true
#ObservedObject var buddies = Buddies()
var body: some View {
VStack {
Text("Simple View")
Button(action: {self.showingSheet.toggle()}) {Image(systemName: "triangle")
}.sheet(isPresented: $showingSheet) {
simpleDetailView(buddies: self.buddies, item: self.buddies.buddy)}
}
}
}
struct simpleContentView_Previews: PreviewProvider {
static var previews: some View {
simpleContentView()
}
}
// End of File: simpleContentView.swift
// This is in a separate file: simpleDetailView.swift
import SwiftUI
struct simpleDetailView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var buddies = Buddies()
var item: Buddy
var body: some View {
VStack {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
Text("First Name = \(item.TheirNames.first)")
Button(action: {self.presentationMode.wrappedValue.dismiss()}){ Text("return"); Image(systemName: "gobackward")}
}
}
}
// ??? Correct way to make preview call
struct simpleDetailView_Previews: PreviewProvider {
static var previews: some View {
// ??? Correct way to call here
simpleDetailView(item: Buddy(TheirNames: TheirNames(first: "", middle: "", last: ""), dob: "", school: "") )
}
}
// end of: simpleDetailView.swift
Using directly #State variable will help you to achieve this, but if you want to sync that variable for both the screens using view model or #Published, this is what you can do. As the #State won't be binded to the #Published property. To achieve this follow these steps.
Step1: - Create a delegate to bind the value on pop or disappearing.
protocol BindingDelegate {
func updateOnPop(value : Int)
}
Step 2:- Follow the code base for Content View
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View , BindingDelegate {
#ObservedObject var input = UserInput()
#State var navIndex : Int? = nil
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1}) {
Text("Adder")
}
ZStack {
NavigationLink(destination: secondScreen(score: self.$input.score,
del: self, navIndex: $navIndex),
tag: 1, selection: $navIndex) {
EmptyView()
}
Button(action: {
self.navIndex = 1
}) {
Text("Next View")
}
}
}
}
}
func updateOnPop(value: Int) {
self.input.score = value
}
}
Step 3: Follow these steps for secondScreen
final class ViewModel : ObservableObject {
#Published var score : Int
init(_ value : Int) {
self.score = value
}
}
struct secondScreen: View {
#Binding var score: Int
#Binding var navIndex : Int?
#ObservedObject private var vm : ViewModel
var delegate : BindingDelegate?
init(score : Binding<Int>, del : BindingDelegate, navIndex : Binding<Int?>) {
self._score = score
self._navIndex = navIndex
self.delegate = del
self.vm = ViewModel(score.wrappedValue)
}
private var btnBack : some View { Button(action: {
self.delegate?.updateOnPop(value: self.vm.score)
self.navIndex = nil
}) {
HStack {
Text("Back")
}
}
}
var body: some View {
VStack {
Text("Button has been pushed \(vm.score)")
Button(action: {
self.vm.score += 1
}) {
Text("Adder")
}
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
}