I'm trying to have a component that basically starts with a single TextField for editing your home phone number, and then you can hit the add button to add different types of phone numbers to your account. for right now I just want to allow the user to edit the phone number, but in the future I'll probably make it so that there are actually two TextFields for each PhoneNumberListItem. 1 field for the editable name and 1 field for the editable phone itself. I'm coming from Android/Compose which is maybe where my line of thinking is stuck. appreciate any pointers.
func ListOfMyPhoneNumbers() -> some View {
#State var listOfMyPhones = [PhoneNumberListItem(name: "home", phone: "123")]
return VStack {
ForEach(listOfMyPhones) { i in
TextField(i.name, text: i.$phone).textFieldStyle(.roundedBorder)
}
Button("Add Phone") {
listOfMyPhones.append(PhoneNumberListItem(name: "other", phone: ""))
}.buttonStyle(.borderless)
Spacer()
}
.padding()
}
struct PhoneNumberListItem: Identifiable {
let id = UUID()
let name: String
#State var phone: String
}
Do not use #State var phone: String in your PhoneNumberListItem.
#State is only for use in a View. Just use a var.
Also use a struct for your view. With the following example code you will be able to edit the TextField in a ForEach, like this:
struct PhoneNumberListItem: Identifiable {
let id = UUID()
var name: String // <--- here
var phone: String // <--- here
}
struct ContentView: View {
var body: some View {
PhoneView() // <--- here
}
}
struct PhoneView: View { // <--- here
#State var listOfMyPhones = [PhoneNumberListItem(name: "home", phone: "123")]
var body: some View {
VStack {
// --- here
ForEach($listOfMyPhones) { $item in
TextField("phone", text: $item.phone)
TextField("name", text: $item.name)
}.textFieldStyle(.roundedBorder)
Button("Add Phone") {
listOfMyPhones.append(PhoneNumberListItem(name: "other", phone: ""))
}.buttonStyle(.borderless)
Spacer()
}
.padding()
}
}
As you progress with learning SwiftUI, you will want to use ObservableObject class to manage your data. Have a look at this link, it gives you some good examples of how to manage data in your app :
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
Related
I have a NavigationSplitView in my app, I have an #State variable in my detail view that gets created in init.
When I select something from the sidebar and the detail view renders, at first everything looks ok. But when I select a different item on the sidebar, the contents of the #state variable don't get recreated.
Using the debugger I can see the init of the detail view get called every time I select a new item in the sidebar, and I can see the #State variable get created. But when it actually renders, the #State variable still contains the previous selection's values.
I've reduced this problem to a test case I'll paste below. The top text in the detail view is a variable passed in from the sidebar, and the second line of text is generated by the #State variable. Expected behavior would be, if I select "one" the detail view would display "one" and "The name is one". If I select "two" the detail view would display "two" and "The name is two".
Instead, if I select "one" first, it displays correctly. But when I select "two", it displays "two" and "The name is one".
Note that if I select "two" as the first thing I do after launching the app, it correctly displays "two" and "The name is two", but when I click on "one" next, it will display "one" and "the name is two". So the state variable is being set once, then never changing again,
Here's the sample code and screenshots:
import SwiftUI
struct Item: Hashable, Identifiable {
let id = UUID()
let name: String
}
struct ContentView: View {
#State private var selectedItem: Item.ID? = nil
private let items = [Item(name: "one"), Item(name: "two"), Item(name: "three")]
func itemForID(_ id: UUID?) -> Item? {
guard let itemID = id else { return nil }
return items.first(where: { item in
item.id == itemID
})
}
var body: some View {
NavigationSplitView{
List(selection: $selectedItem) {
ForEach(items) { item in
Text(item.name)
.tag(item.id)
}
}
} detail: {
if let name = itemForID(selectedItem)?.name {
DetailView(name: name)
} else {
Text("Select an item")
}
}
}
}
struct DetailView: View {
#State var detailItem: DetailItem
var name: String
init(name: String) {
self.name = name
_detailItem = State(wrappedValue: DetailItem(name: name))
}
var body: some View {
VStack {
Text(name)
Text(detailItem.computedText)
}
}
}
struct DetailItem {
let name: String
var computedText: String {
return "The name is \(name)"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Question. What is the purpose of having detailItem as a #State? if you remove the #State, this test case works.
Will the way computedText change over time?
struct DetailView: View {
// #State var detailItem: DetailItem
var detailItem: DetailItem
var name: String
init(name: String) {
self.name = name
// _detailItem = State(wrappedValue: DetailItem(name: name))
detailItem = DetailItem(name: name)
}
var body: some View {
VStack {
Text(name)
Text(detailItem.computedText)
}
}
}
This has nothing to do with NavigationSplitView, but how you initialise #State property.
According to the Apple document on #State (https://developer.apple.com/documentation/swiftui/state):
Don’t initialise a state property of a view at the point in the view hierarchy where you instantiate the view, because this can conflict with the storage management that SwiftUI provides.
As well as the documentation of init(wrappedValue:) (https://developer.apple.com/documentation/swiftui/state/wrappedvalue):
Don’t call this initializer directly. Instead, declare a property with the State attribute, and provide an initial value:
#State private var isPlaying: Bool = false
From my understanding, if you force to initialise the state in the view init, it will persist through the lifetime of the view, and subsequence change of it won't take any effect on the view.
The recommended way in Apple documentation is to create the struct in the parent view and pass it to the child view, and if you need to change the struct in the child view, use #Binding to allow read and write access.
If you want to ignore the documentation and force it to work, you can give an id to your DetailView, forcing it to refresh the view when the item id has changed:
var body: some View {
NavigationSplitView{
List(selection: $selectedItem) {
ForEach(items) { item in
Text(item.name)
.tag(item.id)
}
}
} detail: {
if let name = itemForID(selectedItem)?.name {
DetailView(name: name).id(selectedItem)
} else {
Text("Select an item")
}
}
}
Your Item struct is bad, if the name is unique it should be:
struct Item: Identifiable {
var id: String { name }
let name: String
}
Otherwise:
struct Item: Identifiable {
let id = UUID()
let name: String
}
Hopefully you can see what I'm trying to achieve from the code below but simply put, I'm trying to update .selectedTown which is binded to my Picker. The row tapped on will bind to .selectedTown which will then update the Text 'Your selected town is: [.selectedTown]'
However, the selected row is not binding and the text remains 'Your selected town is: '
struct ContentView: View {
struct Town: Identifiable {
let name: String
let id = UUID()
}
private var towns = [
Town(name: "Bristol"),
Town(name: "Oxford"),
Town(name: "Portsmouth"),
Town(name: "Newport"),
Town(name: "Glasgow"),
]
#State private var selectedTown: String = ""
var body: some View {
NavigationView {
VStack {
Form {
Section {
Picker("", selection: $selectedTown) {
ForEach(towns, id: \.id) {
Text("\($0.name)")
}
}
.pickerStyle(.inline)
.labelsHidden()
} header: {
Text("Random Towns")
}
}
Text("Your selected town is: \(selectedTown)")
.padding()
}
.navigationTitle("Random")
}
}
}
Hopefully this is just a small fix but I've tried for what seems a day to find a solutino and am now stuck. Any help would be gratefully received,
Simon
The types don't match. your array is a towns: [Town] and your selectedTown: String
Option 1 is to change the variable
#State private var selectedTown: Town = Town(name: "Sample")
Option 2 is to add a tag
Text("\($0.name)").tag($0.name)
Option 3 is change the variable and the tag
#State private var selectedTown: Town? = nil
Text("\($0.name)").tag($0 as? Town)
The "best" option depends on what you use selectedTown for.
The type of selection should be same as picked item or use tag, like below
Picker("", selection: $selectedTown) {
ForEach(towns, id: \.id) {
Text("\($0.name)").tag($0.name) // << here !!
}
}
Tested with Xcode 13.2 / iOS 15.2
In my app I´m needing two players (only 2 names/strings), selected from an array built in a List/ForEach SwiftUI-code, which are used in another view.
What is the way to bring the name into a string for my Text(item)?
Can I select two items out of the list?
Thx for any help.
Franz
My code (modified, found by Ale Patron,Tutorial using UserDefaults with encoding and decoding the array/list ):
#State private var allTeams: [PlayerItem] = []
#State private var newPlayer = ""
#State private var selectedPlayer = ""
#State private var selection: String?
struct PlayerItem: Identifiable {
var id = UUID()
let player: String
}
var body: some View {
VStack{
HStack {
TextField("Add Players/Teams...", text: $newPlayer)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.allTeams.append(PlayerItem(player: self.newPlayer))
self.newPlayer = ""
}) {
Image(systemName: "plus")
}
.padding(.leading, 5)
}.padding()
List{
ForEach(allTeams) { playerItem in
Text(playerItem.player)
}
.onTapGesture {
print("How can I select my first und my second player")
}
}
Text("Selected Player: \(selectedPlayer)")
}
}
}
You should use indices for what you are trying to do.
Try this:
ForEach(allTeams.indices) { i in
Text(allTeams[i].player)
.onTapGesture {
print("How can I select my first und my second player")
print("The selected player is \(allTeams[i].player). The second player is \(allTeams[i + 1].player)"
}
}
Make sure to also check if the selected player is the last one in the array and to catch this. Otherwise, you may encounter an out-of-bounds error.
I'm trying to do a NavigationLink within a List or ForEach Loop in SwiftUI. Unfortunately I get a really weird behavior (e.g. when clicking on Leo it opens Karl, Opening Max points to Karl, too).
I've already figured out that it's related to the "isActive" attribute in the NavigationLink. Unfortunately, I need it to achieve a this behavior here: https://i.stack.imgur.com/g0BFz.gif which is also asked here SwiftUI - Nested NavigationView: Go back to root.
I also tried to work with selection and tag attribute but I wasn't able to achieve the "go back to root" mechanics.
Here's the Example:
import SwiftUI
struct Model: Equatable, Hashable {
var userId: String
var firstName: String
var lastName: String
}
struct ContentView: View {
#State var navigationViewIsActive: Bool = false
var myModelArray: [Model] = [
Model(userId: "27e880a9-54c5-4da1-afff-05b4584b1d2f", firstName: "Leo", lastName: "Test"),
Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Max", lastName: "Test"),
Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Karl", lastName: "Test")]
var body: some View {
NavigationView {
List(myModelArray, id: \.self) { model in
NavigationLink(destination: secondView(firstName: model.firstName), isActive: $navigationViewIsActive){ Text(model.firstName) }
}
.listStyle(PlainListStyle())
}
}
}
struct secondView: View {
#State var firstName: String
var body: some View {
NavigationView {
Text(firstName)
.padding()
}
}
}
Thanks!
This happened because of the using of only one state navigationViewIsActive
So when you click in a navigation link , the value will change to True , and all the links will be active
The solution for this scenario is like that :
Define a new State which will hold the selected model value
You need just one NavigationLink , and make it Hidden (put it inside a VStack)
In the List use Button instead of NavigationLink
When a Button is clicked : first change the selectedModel value , than make the navigationLink active (true)
Like the code below (Tested with IOS 14) :
import SwiftUI
struct Model: Equatable, Hashable {
var userId: String
var firstName: String
var lastName: String
}
struct ContentView: View {
#State var navigationViewIsActive: Bool = false
#State var selectedModel : Model? = nil
var myModelArray: [Model] = [
Model(userId: "27e880a9-54c5-4da1-afff-05b4584b1d2f", firstName: "Leo", lastName: "Test"),
Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Max", lastName: "Test"),
Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Karl", lastName: "Test")]
var body: some View {
NavigationView {
VStack {
VStack {
if selectedModel != nil {
NavigationLink(destination: SecondView(firstName: selectedModel!.firstName), isActive: $navigationViewIsActive){ EmptyView() }
}
}.hidden()
List(myModelArray, id: \.self) { model in
Button(action: {
self.selectedModel = model
self.navigationViewIsActive = true
}, label: {
Text(model.firstName)
})
}
.listStyle(PlainListStyle())
}
}
}
}
struct SecondView: View {
#State var firstName: String
var body: some View {
NavigationView {
Text(firstName)
.padding()
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
PS : I wrote this : How to navigate with SwiftUI , it will help you to understand the ways to navigate in swiftUI
You don't need isActive in this case, just use
List(myModelArray, id: \.self) { model in
NavigationLink(destination: secondView(firstName: model.firstName)) {
Text(model.firstName)
}
}
and you have not use NavigationView in second view in this, ie.
struct secondView: View {
var firstName: String // you don't need state here as well
var body: some View {
Text(firstName)
.padding()
}
}
I trying to build an app that having a list that contained some information. I tried to make information partly tappable while the information contained a mobile number or maybe URL? I want to know may it be possible to do so?
For example the URL and mobile in the list
struct ContentView: View {
#State var itemList : [Item] = [Item]()
var body: some View {
VStack{
List(itemList){ item in
Text(item.info)
}
Text("Hello, World!")
}.onAppear(perform: {
print("test")
itemList = [Item(info: "testing"), Item(info: "Contact me 0123456789"), Item(info: "Check our website https://google.com")]
})
}
}
struct Item: Identifiable {
var info : String
let id = UUID()
func load() {
}
}