SwiftUI Picker in reusable component with protocol cannot conform to Hashable - ios

I'm trying to build a reusable component that includes a SwiftUI Picker that can work with different types in several places in my app. I created a Pickable protocol that conforms to Hashable, but when I try to use it, the Picker and the ForEach complain that Type 'any Pickable' cannot conform to 'Hashable'
import SwiftUI
struct PickerRow: View {
let title: String
let options: [any Pickable]
#State var selection: any Pickable
var body: some View {
HStack {
Spacer()
Text(title)
.font(.subHeading)
Picker(title, selection: $selection, content: {
ForEach(options, id: \.self) {
Text($0.name)
}
}).pickerStyle(.menu)
}
}
}
protocol Pickable: Hashable {
var name: String { get }
}
Is there a way to get something like this to work without specifying a concrete type?

If you think about it, it makes sense.
What would you expect to happen if that code was valid and you used it like this?
struct ContentView: View {
let options = [PickableA(), PickableB()]
#State var selection = PickableC()
var body: some View {
PickerRow(title: "Choose one", options: options, selection: $selection)
}
}
That couldn't possibly work right?
What you need is a way to make sure that there is a constraint that forces options and selection to be of the same concrete type (consider Equatable for example, both String and Int are conforming, but you cannot compare them).
One possible solution would be a generic constraint in the declaration of your struct (also note the #Binding instead of #State since we modify external values):
struct PickerRow<Option: Pickable>: View {
let title: String
let options: [Option]
#Binding var selection: Option
var body: some View {
HStack {
Spacer()
Text(title)
.font(.subheadline)
Picker(title, selection: $selection) {
ForEach(options, id: \.self) {
Text($0.name)
}
}.pickerStyle(.menu)
}
}
}
which you could use like this:
struct Person: Pickable {
let name: String
}
struct ContentView: View {
let options = [Person(name: "Bob"), Person(name: "Alice")]
#State var selection = Person(name: "Bob")
var body: some View {
PickerRow(title: "Choose one", options: options, selection: $selection)
}
}

Related

How to pass an Object to new SwiftUI view

I'm having trouble getting started in SwiftUI. What I want to do is rather simple at least I thought it would. What I want to do is that the ContentView expects a first and last name of a person. The button "Add to list" adds the person to a list and the second button shows a list of all added persons in a second view. I read about the property wrappers but I cannot get it to work. Do I need to change struct Person to a class in order to use the #ObservedObject for initialising #StateObject var listOfPersons = [Person]() or is there a more simpler Swift like way to pass the list to my PersonList View?
My project code:
ContentView.swift
struct ContentView: View {
#State var firstName: String = ""
#State var lastName: String = ""
#StateObject var listOfPersons = [Person]()
var body: some View {
NavigationView {
ZStack (alignment: .top){
Color(.orange).opacity(0.2).edgesIgnoringSafeArea(.all)
VStack {
Text("Hello stranger")
.font(.title)
TextField("Fist name:", text: $firstName)
.padding()
TextField("Last name:", text: $firstName)
.padding()
HStack(spacing: 40) {
Button("Add to list") {
listOfPersons.append(Person(firstName: firstName, lastName: lastName))
}
.padding()
NavigationLink(destination: PersonList()) {
Text("Show list")
}
...
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
PersonList.swift
import SwiftUI
struct PersonList: View {
#Binding var listOfPersons = [Person]()
var body: some View {
Color(.orange).opacity(0.2).edgesIgnoringSafeArea(.all)
List(listOfPersons) { person in
PersonRow(person: person) }
}
}
struct PersonList_Previews: PreviewProvider {
static var previews: some View {
PersonList()
}
}
Person.swift
import Foundation
struct Person {
var firstName: String
var lastName: String
}
PersonRow.swift
import SwiftUI
struct PersonRow: View {
var person: Person
var body: some View {
Text("\(person.firstName), \(person.lastName)")
}
}
Your code has a couple problems, but you're on the right track. First, replace #StateObject var listOfPersons = [Person]() with:
#State var listOfPersons = [Person]()
#StateObject is for an instance of a ObservableObject class. #State is what you should be using for a simple array of the Person struct.
Then, in your current code, you're just instantiating a plain PersonList without any parameters. You want to pass in listOfPersons.
/// here!
NavigationLink(destination: PersonList(listOfPersons: $listOfPersons)) {
Text("Show list")
}
The $ sign gets the underlying Binding<[Person]> from listOfPersons, which means that any changes made inside PersonList's listOfPersons will be reflected back to ContentView's listOfPersons.
Finally, in PersonList, change #Binding var listOfPersons = [Person]() to
struct PersonList: View {
#Binding var listOfPersons: [Person]
...
}
#Binding almost never needs to have a default value, since it's always passed in.

Sharing Data between Views in Swift/better approach for this?

I am brand new to Swift and SwiftUi, decided to pick it up for fun over the summer to put on my resume. As a college student, my first idea to get me started was a Check calculator to find out what each person on the check owes the person who paid. Right now I have an intro screen and then a new view to a text box to add the names of the people that ordered off the check. I stored the names in an array and wanted to next do a new view that asks for-each person that was added, what was their personal total? I am struggling with sharing data between different structs and such. Any help would be greatly appreciated, maybe there is a better approach without multiple views? Anyways, here is my code (spacing a little off cause of copy and paste):
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Image("RestaurantPhoto1").ignoresSafeArea()
VStack {
Text("TabCalculator")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom, 150.0)
NavigationLink(
destination: Page2(),
label: {
Text("Get Started!").font(.largeTitle).foregroundColor(Color.white).padding().background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color.blue/*#END_MENU_TOKEN#*/)
})
}
}
}
}
}
struct Page2: View {
#State var nameArray = [String]()
#State var name: String = ""
#State var numberOfPeople = 0
#State var personTotal = 0
var body: some View {
NavigationView {
VStack {
TextField("Enter name", text: $name, onCommit: addName).textFieldStyle(RoundedBorderTextFieldStyle()).padding()
List(nameArray, id: \.self) {
Text($0)
}
}
.navigationBarTitle("Group")
}
}
func addName() {
let newName = name.capitalized.trimmingCharacters(in: .whitespacesAndNewlines)
guard newName.count > 0 else {
return
}
nameArray.append(newName)
name = ""
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
You have multiple level for passing data between views in SwiftUI. Each one has its best use cases.
Static init properties
Binding properties
Environment Objects
Static init properties.
You're probably used to that, it's just passing constants through your view init function like this :
struct MyView: View {
var body: some View {
MyView2(title: "Hello, world!")
}
}
struct MyView2: View {
let title: String
var body: some View {
Text(title)
}
}
Binding properties.
These enables you to pass data between a parent view and child. Parent can pass the value to the child on initialization and updates of this value and child view can update the value itself (which receives too).
struct MyView: View {
// State properties stored locally to MyView
#State private var title: String
var body: some View {
// Points the MyView2's "title" binding property to the local title state property using "$" sign in front of the property name.
MyView2(title: $title)
}
}
struct MyView2: View {
#Binding var title: String
var body: some View {
// Textfield presents the same value as it is stored in MyView.
// It also can update the title according to what the user entered with keyboard (which updates the value stored in MyView.
TextField("My title field", text: $title)
}
}
Environment Objects.
Those works in the same idea as Binding properties but the difference is : it passes the value globally through all children views. However, the property is to be an "ObservableObject" which comes from the Apple Combine API. It works like this :
// Your observable object
class MyViewManager: ObservableObject {
#Published var title: String
init(title: String) {
self.title = title
}
}
struct MyView: View {
// Store your Observable object in the parent View
#StateObject var manager = MyViewManager(title: "")
var body: some View {
MyView2()
// Pass the manager to MyView2 and its children
.environmentObject(manager)
}
}
struct MyView2: View {
// Read and Write access to parent environment object
#EnvironmentObject var manager: MyViewManager
var body: some View {
VStack {
// Read and write to the manager title property
TextField("My title field", text: $manager.title)
MyView3()
// .environmentObject(manager)
// No need to pass the environment object again, it is passed by inheritance.
}
}
}
struct MyView3: View {
#EnvironmentObject var manager: MyViewManager
var body: some View {
TextField("My View 3 title field", text: $manager.title)
}
}
Hope it was helpful. If it is, don't forget to mark this answer as the right one 😉
For others that are reading this to get a better understanding, don't forget to upvote by clicking on the arrow up icon 😄

How to best pass data for form editing in swuiftui (while having that data available to parent view and other subviews)?

I have a SwiftUI form in a MyFormSubView.swift where I have multiple #State variables representing individual fields like Text, etc. My issue is my parent view "ContentView.swift" also needs access to this information, and other subviews "OtherView.swift" also would benefit from access for display or editing. My current approach, is to change all the #State to #Binding, which creates a headache because some forms could have up to 20 fields with some optional... what is the best way to handle this? Is there a way to simply pass an object and have that be 'editable'?
Approaches:
(Current, problem approach) Have multiple individual variables declared as #State
in the ContentView.swift, and pass each individual variable into
MyFormSubView.swift with those variables having #Binding in front of
them that are mapped to swiftui elements to show up as 'placeholder
text' in textboxes, etc. This is bad as I have potentially up to 30 fields with some being optional.
(What I Think I Desire) Have identifiable model with all the
fields (and maybe pass this model into the MyFormSubView.swift, and
if it's possible, bind to it and just have it such that each field
is $mymodel.field1, $mymodel.field2, etc... which eliminates the
need to have 30+ variables passed into this thing.
(Maybe Better?) Use an #ObservableObject.
Is #2 possible? Or is there an even better way? Sample code would be great!
There are several ways to pass data like this across Views. Here is a quick implementation outlining 4 approaches.
You can use an #ObservableObject to reference a class with all of your data inside. The variables are #Published, which allows the View to update in the same way a #State variable would.
You can use an #StateObject. This is the same as #ObservableObject, except it will only initialize once and if the view re-renders the variable will persist (whereas an #ObservedObject would reinitialize). Read more about the difference here.
You can use an #EnvironmentObject. This is the same as #ObservedObject, except it is stored in the Environment, so you don't have to manually pass it between views. This is best when you have a complex view hierarchy and not every view needs a reference to the data.
You can create a custom Model and use a #State variable.
All of these methods work, but based on your description, I'd say the 2nd method is probably best for your situation.
class DataViewModel: ObservableObject {
#Published var text1: String = "One"
#Published var text2: String = "Two"
#Published var text3: String = "Three"
}
struct DataModel {
var text1: String = "Uno"
var text2: String = "Dos"
var text3: String = "Tres"
}
struct AppView: View {
var body: some View {
MainView()
.environmentObject(DataViewModel())
}
}
struct MainView: View {
#StateObject var dataStateViewModel = DataViewModel()
#ObservedObject var dataObservedViewModel = DataViewModel()
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#State var dataStateModel = DataModel()
#State var showSheet: Bool = false
#State var showOtherView: Bool = false
var body: some View {
VStack(spacing: 20) {
Text(dataStateViewModel.text1)
.foregroundColor(.red)
Text(dataObservedViewModel.text2)
.foregroundColor(.blue)
Text(dataEnvironmentViewModel.text3)
.foregroundColor(.green)
Text(dataStateModel.text1)
.foregroundColor(.purple)
Button(action: {
showSheet.toggle()
}, label: {
Text("Button 1")
})
.sheet(isPresented: $showSheet, content: {
FormView(dataStateViewModel: dataStateViewModel, dataObservedViewModel: dataObservedViewModel, dataStateModel: $dataStateModel)
.environmentObject(dataEnvironmentViewModel) // Sheet is a new environment
})
Button(action: {
showOtherView.toggle()
}, label: {
Text("Button 2")
})
if showOtherView {
ThirdView(dataStateViewModel: dataStateViewModel, dataObservedViewModel: dataObservedViewModel, dataStateModel: $dataStateModel)
}
}
}
}
struct FormView: View {
#StateObject var dataStateViewModel: DataViewModel
#ObservedObject var dataObservedViewModel: DataViewModel
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#Binding var dataStateModel: DataModel
#Environment(\.presentationMode) var presentationMode
var body: some View {
Form(content: {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("BACK")
})
Text("EDIT TEXT FIELDS:")
TextField("Placeholder 1", text: $dataStateViewModel.text1)
.foregroundColor(.red)
TextField("Placeholder 2", text: $dataObservedViewModel.text2)
.foregroundColor(.blue)
TextField("Placeholder 3", text: $dataEnvironmentViewModel.text3)
.foregroundColor(.green)
TextField("Placeholder 4", text: $dataStateModel.text1)
.foregroundColor(.purple)
})
}
}
struct ThirdView: View {
#StateObject var dataStateViewModel: DataViewModel
#ObservedObject var dataObservedViewModel: DataViewModel
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#Binding var dataStateModel: DataModel
var body: some View {
VStack(spacing: 20) {
Text(dataStateViewModel.text1)
.foregroundColor(.red)
Text(dataObservedViewModel.text2)
.foregroundColor(.blue)
Text(dataEnvironmentViewModel.text3)
.foregroundColor(.green)
Text(dataStateModel.text1)
.foregroundColor(.purple)
}
}
}
Use ObservalbeObject, here is simple example how you can share data:
Step 1 - Add some kind of state:
class AppState: ObservableObject {
#Published var value: String = ""
}
Step 2 - Pass state to ContentView via setting enviromentObject
ContentView()
.environmentObject(AppState())
Step 3 - Now AppState will be available in all child views of ContentView, so here is the code for ContentView and OtherView. OtherView has the TextField, text from will be saved to AppState and you can be able to see it, when you press 'back' from OtherView.
ContentView:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
Text("Content View")
.padding()
NavigationLink(
destination: OtherView()
) {
Text("Open other view")
}
Text("Value from other view: \(appState.value)")
}
}
}
}
OtherView:
import SwiftUI
struct OtherView: View {
#EnvironmentObject var appState: AppState
#State var value: String = ""
var body: some View {
VStack {
Text("Other View")
.padding()
TextField("Enter value", text: Binding<String>(
get: { self.value },
set: {
self.value = $0
appState.value = $0
}
))
.frame(width: 200)
.padding()
}
}
}
This is just simple example, for more complex cases you can take a look on VIPER or MVVM patterns in Swift UI. For example, here:
https://www.raywenderlich.com/8440907-getting-started-with-the-viper-architecture-pattern
https://www.raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios

Text not updating correctly based on picker

This is my model
struct ListItemModel: Codable, Identifiable {
let id: String
let name: String
}
This is the view that will be displayed with a Picker. The list will be populated by an outside source but I simplified it for this example.
struct TypeSelectionView: View {
#State private var selected = 0
let testList = [ListItemModel(id: "11", name: "name1"),
ListItemModel(id: "12", name: "name2")]
var body: some View {
VStack {
Picker(selection: $selected, label: Text("Pick a Type")) {
ForEach(0 ..< testList.count) {
Text(testList[$0].name)
}
}.labelsHidden()
Picker(selection: $selected, label: Text("Pick a Type")) {
ForEach(testList) {type in
Text(type.name)
}
}.labelsHidden()
Text("Selected Type: \(testList[selected].name)")
Spacer()
}
}
}
struct TypeSelectionView_Previews: PreviewProvider {
static var previews: some View {
TypeSelectionView()
}
}
The first Picker is correctly changing the display of the Text view on the page when the Picker changes but the second Picker does not. Is their a way to make the second Picker do the do the same thing where as you change the Picker the Text view will update accordingly
or is the first Picker the way you should always go when making Pickers in SwiftUI?
The reason your second Picker doesn't work is that the values returned by the Picker correspond to the id of the items. In the case of your second Picker, those are String.
You can apply a .tag() to each item, and then the Picker will return that. For example, if you added an explicit tag it would work:
Text(type.name).tag(testList.firstIndex(where: { $0.id == type.id })!)
Alternatively, if you changed your id values to be Int and the id values corresponded to the position in the array, it would work.
Because of the difficulties of implementing a tag, it is easy to see why many developers choose to just iterate on 0 ..< testList.count.
Ok so this my first ever answer for a stack overflow question, I'm quite a newbie myself but hopefully I can be of some help.
The code when placed in to Xcode shows two pickers whose initial values are name1 but when you change the first picker the second picker and the text displaying the selected type change accordingly, but because both pickers share the same source of truth #State private var selected = 0, changing this will have unintended side effects.
import SwiftUI
struct TypeSelectionView: View {
#State private var selected = 0
#State var testList = [ListItemModel(id: "11", name: "name1"),
ListItemModel(id: "12", name: "name2")]
#State var priorityTypes = ["low", "medium", "high", "critical"]
var body: some View {
VStack {
Picker("Pick a Type", selection: $selected) {
ForEach(0..<testList.count) {
Text(self.testList[$0].name)
}
}.labelsHidden()
Picker("Pick a Type", selection: $selected) {
ForEach(0..<testList.count) {
Text(self.testList[$0].name)
}
}.labelsHidden()
Text("Selected Type: \(testList[selected].name)")
Spacer()
}
}
}
struct TypeSelectionView_Previews: PreviewProvider {
static var previews: some View {
TypeSelectionView()
}
}
struct ListItemModel: Codable, Identifiable {
let id: String
let name: String
}

SwiftUI: heterogeneous collection of destination views for NavigationLink?

I’m building like a demo app of different examples, and I’d like the root view to be a List that can navigate to the different example views. Therefore, I tried creating a generic Example struct which can take different destinations Views, like this:
struct Example<Destination: View> {
let id: UUID
let title: String
let destination: Destination
init(title: String, destination: Destination) {
self.id = UUID()
self.title = title
self.destination = destination
}
}
struct Example1View: View {
var body: some View {
Text("Example 1!")
}
}
struct Example2View: View {
var body: some View {
Text("Example 2!")
}
}
struct ContentView: View {
let examples = [
Example(title: "Example 1", destination: Example1View()),
Example(title: "Example 2", destination: Example2View())
]
var body: some View {
List(examples, id: \.id) { example in
NavigationLink(destination: example.destination) {
Text(example.title)
}
}
}
}
Unfortunately, this results in an error because examples is a heterogeneous collection:
I totally understand why this is broken; I’m creating a heterogeneous array of examples because each Example struct has its own different, strongly typed destination. But I don’t know how to achieve what I want, which is an array that I can make a List out of which has a number of different allowed destinations.
I’ve run into this kind of thing in the past, and in the past I’ve gotten around it by wrapping my generic type and only exposing the exact properties I needed (e.g. if I had a generic type that had a title, I would make a wrapper struct and protocol that exposed only the title, and then made an array of that wrapper struct). But in this case NavigationLink needs to have the generic type itself, so there’s not a property I can just expose to it in a non-generic way.
You can use the type-erased wrapper AnyView. Instead of making Example generic, make the destination view inside of it be of type AnyView and wrap your views in AnyView when constructing an Example.
For example:
struct Example {
let id: UUID
let title: String
let destination: AnyView
init(title: String, destination: AnyView) {
self.id = UUID()
self.title = title
self.destination = destination
}
}
struct Example1View: View {
var body: some View {
Text("Example 1!")
}
}
struct Example2View: View {
var body: some View {
Text("Example 2!")
}
}
struct ContentView: View {
let examples = [
Example(title: "Example 1", destination: AnyView(Example1View())),
Example(title: "Example 2", destination: AnyView(Example2View()))
]
var body: some View {
NavigationView {
List(examples, id: \.id) { example in
NavigationLink(destination: example.destination) {
Text(example.title)
}
}
}
}
}

Resources