I have been trying to create smaller reusable view for a simple SwiftUI project. However, I'm getting unexpected results and struggling to understand why.
Here is my contrived example to highlight the issue
Using this example, how should you create a reusable RowView to take place of the HStack in a list
var body: some View {
List(vm.gradings) { item in
// how should you refactor out this ??
HStack {
Text(item.grade)
Text(item.pass ? "Pass" : "Fail")
}
}
}
I'm using Firebase to trigger a model change. i.e changing a grade to pass or fail and expecting the list detail view to display that change.
The above method works as expected, while on the list view and the row will change when the data is changed.
However, when I try and refactor that view out, only one works as expected. Ideally, I'd like to pass in the Grading object, but that doesn't cause the view to refresh.
Trying different subviews and their results
var body: some View {
List(vm.gradings) { item in
// Works
RowA(grade: item.grade, pass: item.pass)
// Fails
// RowB(item: item)
// Fails
// RowC(item: item)
// Default - works as expected
// HStack {
// Text(item.grade)
// Text(item.pass ? "Pass" : "Fail")
// }
}
}
Here are the 3 row options
// Works
struct RowA: View {
var grade: String
var pass: Bool
var body: some View {
HStack {
Text(grade)
Text(pass ? "Pass" : "Fail")
}
}
}
// Fails
struct RowB: View {
var item: Grading
var body: some View {
HStack {
Text(item.grade)
Text(item.pass ? "Pass" : "Fail")
}
}
}
// Fails
struct RowC: View {
#State var item: Grading
var body: some View {
HStack {
Text(item.grade)
Text(item.pass ? "Pass" : "Fail")
}
}
}
Work Around
// Allows me to pass in just the model
struct RowD: View {
private var item = Grading()
private var grade: String = ""
private var pass: Bool = false
init(item: Grading) {
self.item = item
self.grade = item.grade
self.pass = item.pass
}
var body: some View {
HStack {
Text(grade)
Text(pass ? "Pass" : "Fail")
}
}
}
My view model
class StudentGradingsUIViewModel: ObservableObject {
#Published var detailedstudent: DetailedStudent
var gradings: [Grading] {
detailedstudent.student.gradings
}
init(student: DetailedStudent) {
self.detailedstudent = student
}
}
If you use a #state, I found all what you are doing is right at least in these View portion.
/ Works
struct RowA: View {
var grade: String
var pass: Bool
var body: some View {
HStack {
Text(grade)
Text(pass ? "Pass" : "Fail")
}
}
}
//// Fails
struct RowB: View {
var item: Grading
var body: some View {
HStack {
Text(item.grade)
Text(item.pass ? "Pass" : "Fail")
}
}
}
//
//// Fails
struct RowC: View {
#State var item: Grading
var body: some View {
HStack {
Text(item.grade)
Text(item.pass ? "Pass" : "Fail")
}
}
}
struct Grading: Identifiable{
var id = UUID()
var grade : String
var pass : Bool
}
struct TempGradingView: View {
#State var gradings: [Grading] = [Grading.init(grade: "50", pass: false),Grading.init(grade: "100", pass: true),Grading.init(grade: "100", pass: true)]
var body: some View {
Group{
Button("change", action: {
self.gradings[0].grade = "70"
})
List(gradings) { item in
// Works
// RowA(grade: item.grade, pass: item.pass)
// Fails
RowB(item: item)
// Fails
// RowC(item: item)
// Default - works as expected
// HStack {
// Text(item.grade)
// Text(item.pass ? "Pass" : "Fail")
// }
}}
}
}
So I think the problematic part, i.e. #publisher var. part.
This works - wondering I think its a decent approach
private func gradingRow(_ grading: Grading) -> some View {
HStack {
Text(grading.grade)
.foregroundColor(grading.pass ? Color(UIColor.systemGreen) : Color(UIColor.systemRed))
Spacer()
Text(DateString.dateAsDDMMYYTime(from: grading.date))
.font(.subheadline)
}
}
Related
A week into learning SwiftUI, this is probably a simple error I'm making but can't figure it out… Trying to separate my views from model etc. However, when I call my view I get the error "Cannot convert value of type 'PuzzleView' to expected argument type 'Puzzle'".
My model is:
struct Puzzle : Codable, Identifiable {
var id: String
var region: String
var score: Int
var wordCount: Int
var pangramCount: Int
var foundPangrams: [String]
var solvedScore: Int
}
class PuzzleData : ObservableObject {
#Published var puzzles: [Puzzle]
init (puzzles: [Puzzle] = []) {
self.puzzles = puzzles
}
}
ContentView (no errors)
struct ContentView: View {
#StateObject var puzzleData : PuzzleData = PuzzleData(puzzles: getJson)
var body: some View {
NavigationView {
List {
ForEach (puzzleData.puzzles) { puzzle in
ListPuzzles(puzzle: puzzle)
}
}
.navigationBarTitle(Text("Puzzles"))
}
}
}
And the problem file with error:
struct PuzzleView: View {
let selectedPuzzle: Puzzle
var body: some View {
VStack {
Group {
Text(selectedPuzzle.id)
.font(.headline)
HStack {
DataRow(selectedPuzzle: Puzzle) //<<<<error here
}
Text(selectedPuzzle.region)
.font(.body)
}
}
}
}
The file it is linking to is:
struct DataRow: View {
var selectedPuzzle: Puzzle
var body: some View {
HStack {
Spacer()
Group {
VStack {
Text("Score")
Text("\(selectedPuzzle.solvedScore)/\\\(selectedPuzzle.score)")
}
VStack {
Text("Words")
Text("\(selectedPuzzle.foundWords.count - 1)/\\\(selectedPuzzle.wordCount)")
}
VStack {
Text("\((selectedPuzzle.pangramCount != 1) ? "Pangrams:" : "Pangram:")")
Text("\(selectedPuzzle.foundPangrams.count - 1)/\\\(selectedPuzzle.pangramCount)")
}
}
}
}
}
Will really appreciate any advise, thanks!
So let's say I have a list component in SwiftUI:
struct MyListView: View {
var body: some View {
List(...) { rec in
Row(rec)
}
}
}
Now let's say I want to make this reusable, and I want the "caller" of this view to determine what happens when I tap on each row view. What would be the correct way to insert that behavior?
Here is some other Buttons in ListView example that you can run and play with it yourself
import SwiftUI
struct TestTableView: View {
#State private var item: MyItem?
var body: some View {
NavigationView {
List {
// Cell as Button that display Sheet
ForEach(1...3, id:\.self) { i in
Button(action: { item = MyItem(number: i) }) {
TestTableViewCell(number: i)
}
}
// Cell as NavigationLink
ForEach(4...6, id:\.self) { i in
NavigationLink(destination: TestTableViewCell(number: i)) {
TestTableViewCell(number: i)
}
}
// If you want a button inside cell which doesn't trigger the whole cell when being touched
HStack {
TestTableViewCell(number: 7)
Spacer()
Button(action: { item = MyItem(number: 7) }) {
Text("Button").foregroundColor(.accentColor)
}.buttonStyle(PlainButtonStyle())
}
}
}.sheet(item: $item) { myItem in
TestTableViewCell(number: myItem.number)
}
}
struct MyItem: Identifiable {
var number: Int
var id: Int { number }
}
}
struct TestTableViewCell: View {
var number: Int
var body: some View {
Text("View Number \(number)")
}
}
Make it like Button and takes an action param that is a closure.
From my understanding you're looking for a reusable generic List view with tap on delete functionality. If I'm guessing right my approach then would be like this:
struct MyArray: Identifiable {
let id = UUID()
var title = ""
}
struct ContentView: View {
#State private var myArray = [
MyArray(title: "One"),
MyArray(title: "Two"),
MyArray(title: "Three"),
MyArray(title: "Four"),
MyArray(title: "Five"),
]
var body: some View {
MyListView(array: myArray) { item in
Text(item.title) // row view
} onDelete: { item in
myArray.removeAll(where: {$0.id == item.id}) // delete func
}
}
}
struct MyListView<Items, Label>: View
where Items: RandomAccessCollection, Items.Element: Identifiable, Label: View {
var array: Items
var row: (Items.Element) -> Label
var onDelete: (Items.Element) -> ()
var body : some View {
List(array) { item in
Button {
onDelete(item)
} label: {
row(item)
}
}
}
}
Is there a way to delete all the list items in SwiftUI?
I'm using a ForEach() inside a List() and I want to have a clear all button to remove all the items from the list, is there a way to do it?
struct SwiftUIView: View {
#State var filters : [filter] = [filter(name: "new"), filter(name: "old"), filter(name: "some")]
#State var afterFilters : [someFilter] = []
var body: some View {
List{
ForEach(0..<self.filters.count, id:\.self){ i in
filterRepresent(string: self.$afterFilters[i].filter.name, isOn: self.$afterFilters[i].isOn)
}
}.onAppear {
for filter in self.filters {
self.afterFilters.append(someFilter(filter: filter))
}
}
}
}
struct filterRepresent : View {
#Binding var string : String
#Binding var isOn : Bool
var body : some View {
HStack{
Text(string)
Toggle("",isOn: $isOn)
}
}
}
struct filter {
var name : String
var isOn : Bool
init(name: String){
self.name = name
self.isOn = false
}
}
struct someFilter : Identifiable{
var id : Int
var filter : filter
var isOn : Bool
init(filter : filter){
self.id = Int.random(in: 0...100000)
self.filter = filter
self.isOn = filter.isOn
}
}
As you can see, in the example above, I'm using a #Binding to change the data I store based on the Toggle state, I want to have a button that deletes the entire list (in the real app the data to the list is uploaded from a server side into a temp array just like in the above) when I do it with .removeall() I get thrown with "out of index" error.
The button I use :
Button(action: {
self.afterFilters.removeAll()
}, label: {
Text("Clear all").font(Font.custom("Quicksand-Medium", size: 15))
})
The error I'm getting:
Fatal error: Index out of range: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.13/swift/stdlib/public/core/ContiguousArrayBuffer.swift, line 444
You have to clean up model and view will be refreshed automatically.
Here is a simple demo:
struct DemoCleanUpList: View {
#State private var persons = ["Person 1", "Person 2", "Person 3"]
var body: some View {
VStack {
Button("CleanUp") { self.persons.removeAll() }
List {
ForEach(persons, id: \.self) { person in
Text(person)
}
}
}
}
}
I fetched JSON data from Google Sheet and populate into a List using ForEach. I used struct HeaderView located in another View and place a Button to serve as a toggle. However, the List will not redraw when I press the toggle button even I use #State ascd variable.
Below is some of my code, is there anything I miss?
struct HeaderView: View {
// #State var asc: Bool = true
var holding: String = "持倉"
var earning: String = "賺蝕"
// #State var tog_value: Bool = ContentView().ascd
var body: some View {
HStack {
Button(action: {
ContentView().ascd.toggle()
}
) {
Text("Button")
}
Text(holding)
Text(earning)
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
#ObservedObject var viewModelTotal = ContentViewModelTotal()
#State var ascd: Bool = false
var totalss = ContentViewModelTotal.fetchDatasTotal
var body: some View {
List {
Section(header: HeaderView()) {
ForEach(viewModel.rows, id: \.stockname) { rows in
// Text(user.stock_name)
ListRow(name: rows.stockname, code: rows.stockcode, cur_price: rows.currentprice, mkt_value: rows.marketvalue, amnt: rows.amount, avg_cost: rows.averagecost, pft: rows.profit, pft_pcnt: rows.profitpercent)
}
}
.onAppear {
self.viewModel.fetchDatas()
self.ascd.toggle()
if self.ascd {
self.viewModel.rows.sort { $0.stockname < $1.stockname }
} else {
self.viewModel.rows.sort { $0.stockname > $1.stockname }
}
}
}
}
}
For changing another View's variable you can use a #Binding variable:
struct HeaderView: View {
...
#Binding var ascd: Bool
var body: some View {
HStack {
Button(action: {
self.ascd.toggle()
}) {
Text("Button")
}
Text(holding)
Text(earning)
}
}
}
I'd recommend moving sorting logic to your ViewModel.
class ContentViewModel: ObservableObject {
#Published var ascd: Bool = false {
didSet {
if ascd {
rows.sort { $0.hashValue < $1.hashValue }
} else {
rows.sort { $0.hashValue > $1.hashValue }
}
}
}
...
}
If it's in the .onAppear in the ContentView it will be executed only when your View is shown on the screen.
And you will have to initialise your HeaderView with your ViewModel's ascd variable:
HeaderView(ascd: $viewModel.ascd)
I am trying to set the value of a #State var in var a of struct A from a var b of struct B, but it doesn't work. I need to use a #State var because I am passing it as a binding. Ex:
struct A : View {
#State var myBindableVar = ""
var body : some View {
TextField(self.$myBindableVar) ...
}
}
struct B : View {
#State var a : A
var body : some View {
Button(action: { self.a.myBindableVar = "???" }) { ... }
}
}
myBindableVar isn't set to "???" when the button is tapped. Why?
You need to use #Binding to achieve this. Here is some example code. I let View B appear inside View A so that you can directly see the working result on screen:
struct A : View {
#State var myBindableVar = ""
var body : some View {
VStack {
Text(myBindableVar)
Spacer()
B(myBindableVar: $myBindableVar)
}
}
}
struct B : View {
#Binding var myBindableVar : String
var body : some View {
Button(action: { self.myBindableVar = "Text appears" }) {
Text("Press to change")
}
}
}