Using the SwiftUI framework, I created this reusable View (minus styling bells and whistles):
struct RatingView: View {
let criteria: String
#State var rating: Int
var body: some View {
HStack {
Button {
if rating > 0 {
rating -= 1
}
} label: {
Image(systemName: "heart.slash.fill")
}
Spacer()
Text("\(criteria): \(rating)")
Spacer()
Button {
if rating < 1 {
rating += 1
}
} label: {
Image(systemName: "heart.fill")
}
}
}
}
And implemented it here (again, without the styling bells and whistles):
struct ScoringView: View {
let story = "Story"
let cheese = "Cheese"
let love = "Romance"
let actor = "In character"
var storyRating = 0
var cheesyRating = 0
var loveRating = 0
var actorRating = 0
var roundScore: Int {
storyRating + cheesyRating + loveRating + actorRating
}
var body: some View {
VStack {
RatingView(criteria: story, rating: storyRating)
RatingView(criteria: cheese, rating: cheesyRating)
RatingView(criteria: love, rating: loveRating)
RatingView(criteria: actor, rating: actorRating)
}
Spacer()
VStack {
Text("Score this round:")
Text("\(roundScore)")
}
}
}
No matter the changes I make to the rating buttons, the roundScore computed property does not change and remains at zero.
I tested the computed property in a Playground, and it works! But since I wasn't able to find anything on SO about the combo of computed properties and reusable views, so I wonder if the reusable view is messing with me.
Any thoughts?
You need something yo trigger View updates or SwiftUI doesn't know when to redraw.
Add #State
#State var storyRating = 0
#State var cheesyRating = 0
#State var loveRating = 0
#State var actorRating = 0
and remove it from RatingView
#Binding var rating: Int
#State is a source of truth and #Binding is a two-way connection.
You will also have to add $ when initializing views
RatingView(criteria: story, rating: $storyRating)
RatingView(criteria: cheese, rating: $cheesyRating)
RatingView(criteria: love, rating: $loveRating)
RatingView(criteria: actor, rating: $actorRating)
Related
I'm using TabView to display a scrollable tab list of players to update. I am trying to focus the newScoreEntry field upon scrolling to the next TabView. I am having trouble figuring out how to focus the right field based upon my selectedTab. I think I have to somehow define my focusfield as an array of fields or something?
struct ScoreRoundView: View {
#StateObject var game: Game
#State var newScoreEntry: [String] = Array(repeating: "", count: 50)
#State var selectedTab = 0
#FocusState private var focusScore: Bool
var body: some View {
TabView(selection: $selectedTab) {
ForEach(Array(zip(game.playersArray.indices, game.playersArray)), id: \.1) { i, player in
TextField("Round Score", text: $newScoreEntry[i])
.focused($focusScore)
}.tag(i)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.onChange(of: selectedTab, perform: { value in
focusScore = true
})
}
}
I figured out what i was looking for, so i thought i would post the solution. I had to use focusable field with an id associated with it.
enum Focusable: Hashable {
case tabView(id: Int)
}
#FocusState var focusedField: Focusable?
#StateObject var game: Game
#State var newScoreEntry: [String] = Array(repeating: "", count: 50)
#State var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
ForEach(Array(zip(game.playersArray.indices, game.playersArray)), id: \.1) { i, player in
TextField("Round Score", text: $newScoreEntry[i])
.focused($focusedField, equals: .tabView(id: i))
}.tag(i)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.onChange(of: selectedTab, perform: { value in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.focusedField = .tabView(id: value)
}
})
I have multiple views created by a ForEACH. Each View has a textfield where a user can enter a number. I would like to subtotal each entry in each view. In other words subtotal the binding in each view.
Is my approach wrong?
ForEach(someArray.allCases, id: \.id) { item in
CustomeRowView(name: item.rawValue)
}
struct CustomeRowView: View {
var name: String
#State private var amount: String = ""
var body: some View {
VStack {
HStack {
Label(name, systemImage: image)
VStack {
TextField("Amount", text: $amount)
.frame(width: UIScreen.main.bounds.width / 7)
}
}
}
}
}
Any help would be greatly appreciated.
there are many ways to achieve what you ask. I present here a very
simple approach, using an ObservableObject to keep the info in one place.
It has a function to add to the info dictionary fruits.
A #StateObject is created in ContentView to keep one single source of truth.
It is passed to the CustomeRowView view using #ObservedObject, and used to tally
the input of the TextField when the return key is pressed (.onSubmit).
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class FruitCake: ObservableObject {
#Published var fruits: [String : Int] = ["apples":0,"oranges":0,"bananas":0]
// adjust for you purpose
func add(to name: String, amount: Int) {
if let k = fruits.keys.first(where: {$0 == name}),
let sum = fruits[k] {
fruits[k] = sum + amount
}
}
}
struct ContentView: View {
#StateObject var fruitCake = FruitCake()
var body: some View {
VStack {
ForEach(Array(fruitCake.fruits.keys), id: \.self) { item in
CustomeRowView(name: item, fruitCake: fruitCake)
}
}
}
}
struct CustomeRowView: View {
let name: String
#ObservedObject var fruitCake: FruitCake
#State private var amount = 0
var body: some View {
HStack {
Label(name, systemImage: "info")
TextField("Amount", value: $amount, format: .number)
.frame(width: UIScreen.main.bounds.width / 7)
.border(.red)
.onSubmit {
fruitCake.add(to: name, amount: amount)
}
// subtotal
Text("\(fruitCake.fruits[name] ?? 0)")
}
}
}
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've never done a post on this before, so hopefully this is set up correctly.
I'm new to swift, and I want to create a button that adds a new item to a list that contains a navigation link to a file that it would create with linked data from the previous item, and I haven't found any way to do this after spending a few days on research and testing.
This is what my app currently looks like for the layout I want in the end: Q1 , and here is a preview of the different Q1-4 Views I mentioned: Q1-4
I know it's a lot, so let me explain more in depth: I want to have a list contained in what is called 'Q1' (as seen above) that starts out with 'Week 1', and as you click the add button, I want it to add a 'Week 2' and so forth up to 10 weeks. Once you hit 10 weeks, I want the user to have to change to the different view, 'Q2', which then they can add Week 11-20, and so forth until Q4, which limits it to a total of 40 Weeks. I want each week to contain a navigation link to a new view; however, I also want data from the previous week to be carried over as soon as I create the new week, so the user won't have to manually put in the previous week's data.
I know how to do some of this by using a JSON file for the numbers, as I've seen tutorials on that, however, I don't see a point to this, as the only data I need for the Week numbers are 1-40, but I can't seem to get it to work with an array or anything. I do know that I can use an #EnvironmentObject to get the data I need from the other pages, but I'm not exactly sure how to set that up either. Other than that, I'm stuck! Here is my code:
import SwiftUI
struct BillsView: View {
#State private var quarterNumber = 0
let quarterNumbers = [1, 2, 3, 4]
var body: some View {
NavigationView{
VStack {
Section {
Picker("Quarter Number", selection: $quarterNumber) {
ForEach(0 ..< quarterNumbers.count) {
Text("Q\(self.quarterNumbers[$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
if quarterNumber == 0 {
Q1View()
} else if quarterNumber == 1 {
Q2View()
} else if quarterNumber == 2 {
Q3View()
} else if quarterNumber == 3 {
Q4View()
}
}
Spacer()
}
.navigationBarTitle("Bills")
.navigationBarItems(leading: EditButton(),
trailing: Button(action: {
//Adds the new week
}){
Image(systemName: "plus.circle.fill")
})
}
}
}
struct Q1View: View {
#State private var weekNumber = 0
let weekNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var body: some View {
List {
NavigationLink(destination: Week1View()) {
Text("Week 1")
}
}
}
}
struct Week1View: View {
var body: some View {
List {
link(label: "Gross Income", destination: GrossIncome())
link(label: "Expenses", destination: Expenses())
}.navigationBarTitle(Text("Week 1"), displayMode: .inline)
}
private func link<Destination: View>(label: String, destination: Destination) -> some View {
return NavigationLink(destination: destination) {
Text(label)
}
}
}
I am not quite sure whether i understood you right, but i made a very simple example which answers the question of your title
import SwiftUI
struct ContentView: View {
#State private var list : [String] = ["Chris"]
#State private var quarterNumber = 0
var body: some View {
Group () {
Button(action: {
self.list.append("whatever")
}) {
Text("tap me")
}
NavigationView{
List(list, id: \.self) { item in
NavigationLink(
destination: View2(text: "hallo")
.navigationBarTitle(Text("Categories"), displayMode: .automatic)
) {
Text("Categories")
}.isDetailLink(false) // damit der Doof nicht rechts das nächste Menu öffnet
}
}
}
}
}
struct View2: View {
var text : String
var body: some View {
Text(text)
}
}