I'm struggling to pass the right data to another View.
The idea is first to select the Task that has Status 0 and pass it to CardContentView to display under the Section of New tasks. If I print the values, its correct but it always displays the first data/array regardless of its Status. What could be done here?
struct Tasks: Identifiable {
let id = UUID()
let name: String
let status: Int
let image: String
}
extension Tasks {
static var testData: [Tasks] {
return [
Tasks(name: "Inprogress", status: 1, image:"a1"),
Tasks(name: "New", status: 0, image:"a2"),
]
}
}
ContentsView
struct ContentsView: View {
#State var items: [Tasks]
var size = 0
var body: some View {
NavigationView {
List {
let new = items.filter({$0.status == 0})
let size = new.count
if size > 0 {
Section(header:Text("\(size)" + " New")){
//let _ = print(new)
ForEach(new.indices, id: \.self) {itemIndex in
NavigationLink(destination: ChosenTask()) {
CardContentView(item: self.$items[itemIndex])
}
}
}
}
}
.navigationBarTitle("My tasks")
}
}
}
CardContentView
struct CardContentView: View {
#Binding var item: Tasks
var body: some View {
HStack {
VStack(alignment: .leading,spacing: 5) {
Label("Name: " + (item.name), systemImage: "person.crop.circle")
.font(.system(size: 12))
.labelStyle(.titleAndIcon)
}
.frame(maxWidth: .infinity, alignment: .leading)
Image(item.image)
.resizable()
.frame(width: 60, height: 70)
}
}
}
You are already passing the item to another view when you call CardContentView. You just have to do the same thing and pass the item to ChosenTask in your NavigationLink. When the user taps the item, SwiftUI will take care of creating and displaying the ChoseTask view for you.
You should also avoid using indices. There is no need. Your struct conforms to Identifiable so you can use it directly
var body: some View {
NavigationView {
List {
let new = items.filter({$0.status == 0})
if !new.isEmpty {
Section(header:Text("\(size)" + " New")){
//let _ = print(new)
ForEach(new) {item in
NavigationLink(destination: ChosenTask(item: item)) {
CardContentView(item: item)
}
}
}
}
}
.navigationBarTitle("My tasks")
}
}
Related
First Dabble in SwiftUI, I've managed to get the below code working such that when I press a button, it will show a "selected" state and add the selected sports into an array. (and remove from the array if "deselected")
However, I can't figure out how to initialise the sportsArray with ALL values within the enum HelperIntervalsIcu.icuActivityType.allCases if it is initially empty.
I tried to put in
if sportsArray.isEmpty {
HelperIntervalsIcu.icuActivityType.allCases.forEach {
sportsArray.append($0.rawvalue)
}
but Xcode keeps telling me type() cannot conform to View or things along those lines
struct selectSports: View {
#State private var sportsArray = [String]()
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
// https://stackoverflow.com/a/69455949/14414215
ForEach(Array(HelperIntervalsIcu.icuActivityType.allCases), id:\.rawValue) { sport in
Button(action: {
addSports(sport: sport.rawValue)
}) {
HStack {
Image(getSportsIcon(sport: sport.rawValue))
.selectedSportsImageStyle(sportsArray: sportsArray, sport: sport.rawValue)
Text(sport.rawValue)
}
}
.buttonStyle(SelectedSportButtonStyle(sportsArray: sportsArray, sport: sport.rawValue))
}
}
}
}
struct SelectedSportButtonStyle: ButtonStyle {
var sportsArray: [String]
var sport: String
var selectedSport : Bool {
if sportsArray.contains(sport) {
return true
} else {
return false
}
}
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.font(selectedSport ? Font.subheadline.bold() : Font.subheadline)
.aspectRatio(contentMode: .fit)
.foregroundColor(selectedSport ? Color.orange : Color(UIColor.label))
.padding([.leading, .trailing], 15)
.padding([.top, .bottom],10)
.overlay(
RoundedRectangle(cornerRadius: 5.0)
.stroke(lineWidth: 2.0)
.foregroundColor(selectedSport ? Color.orange : Color.gray)
)
.offset(x: 10, y: 0)
}
}
func addSports(sport: String) {
if sportsArray.contains(sport) {
let sportsIndex = sportsArray.firstIndex(where: { $0 == sport })
sportsArray.remove(at: sportsIndex!)
} else {
sportsArray.append(sport)
}
print("Selected Sports:\(sportsArray)")
}
}
No Sports Selected (in this case sportsArray is empty and thus the default state which I would like have would be to have ALL sports Pre-selected)
2 Sports Selected
I assume you wanted to do that in onAppear, like
struct selectSports: View {
#State private var sportsArray = [String]()
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
// .. other code
}
.onAppear {
if sportsArray.isEmpty { // << here !!
HelperIntervalsIcu.icuActivityType.allCases.forEach {
sportsArray.append($0.rawvalue)
}
}
}
}
}
So I have a ScrollView that contains a list of all the contacts imported from a user's phone. Above the ScrollView, I have a 'filter search bar' that has a binding that causes the list to show only contacts where the name contains the same string as the search bar filter. For some reason, the last two contacts in the list always pop up at the bottom of the list, no matter what the string is (even if it's a string not contained in any of the contact names on the phone). I tried deleting a contact and the problem persists, because the original contact was just replaced with the new second to last contact. Any help fixing this would be much appreciated!
struct SomeView: View {
#State var friendsFilterText: String = ""
#State var savedContacts: CustomContact = []
var body: some View {
var filteredContactsCount = 0
if friendsFilterText.count != 0 {
for contact in appState.savedContacts {
if contact.name.lowercased().contains(friendsFilterText.lowercased()) {
filteredContactsCount += 1
}
}
} else {
filteredContactsCount = savedContacts.count
}
return HStack {
Image(systemName: "magnifyingglass")
ZStack {
HStack {
Text("Type a name...")
.opacity(friendsFilterText.count > 0 ? 0 : 1)
Spacer()
}
CocoaTextField("", text: $friendsFilterText)
.background(Color.clear)
}
Button(action: {
friendsFilterText = ""
}, label: {
Image(systemName: "multiply.circle.fill")
})
}.frame(height: 38)
HStack(spacing: 10) {
Text("Your contacts (\(filteredContactsCount))")
Spacer()
Button(action: {
fetchContacts()
}, label: {
Image(systemName: "square.and.arrow.down")
})
Button(action: {
// edit button action
}, label: {
Text("Edit")
})
}
ScrollView {
VStack {
ForEach(savedContacts, id: \.self.name) { contact in
if contact.name.lowercased().contains(friendsFilterText.lowercased()) || friendsFilterText.count == 0 {
Button(action: {
// contact button action
}, label: {
HStack(spacing: 20) {
Image(systemName: "person.crop.circle.fill")
.font(.system(size: 41))
.frame(width: 41, height: 41)
VStack(alignment: .leading, spacing: 4) {
Text(contact.name)
Text(contact.phoneNumber)
}
Spacer()
}.frame(height: 67)
})
}
}
}
}
}
}
CustomContact is a custom struct with properties phoneNumber and name. I've attached images below of the issue I'm experiencing. I'm thinking MAYBE it's because there's something off timing-wise with the friendsFilterText and the ForEach rendering but I'm really not sure.
In the image set below, the 'Extra Contact 1' and 'Extra Contact 2' are ALWAYS rendered, unless I add a filter, then switch to a different view, then back to this view (which leads me to believe it's a timing thing again).
https://imgur.com/a/CJW2CUS
You should move the count calculation out of the view into a computed var.
And if CustomContact is your single contact struct, it should actually read #State var savedContacts: [CustomContact] = [] i.e. an array of CustomContact.
The rest worked fine with me, no extra contacts showing.
struct ContentView: View {
#State var friendsFilterText: String = ""
#State var savedContacts: [CustomContact] = []
// computed var
var filteredContactsCount: Int {
if friendsFilterText.isEmpty { return savedContacts.count }
return savedContacts.filter({ $0.name.lowercased().contains(friendsFilterText.lowercased()) }).count
}
var body: some View {
...
For some reason, my selectedTask State is Empty when presenting the Sheet,
even if I set it on the onTapGesture.
What I'm I missing?
struct TasksTabView: View {
#State private var showComputedTaskSheet: Bool = false
#State var selectedTask: OrderTaskCheck?
var body: some View {
VStack(alignment: .leading) {
List {
ForEach(Array(tasks.enumerated()), id:\.1.title) { (index, task) in
VStack(alignment: .leading, spacing: 40) {
HStack(spacing: 20) {
PillForRow(index: index, task: task)
}.padding(.bottom, 30)
}.onTapGesture {
// Where I'm setting selectedTask
self.selectedTask = task
self.showComputedTaskSheet.toggle()
}
}
}
}.listStyle(SidebarListStyle())
}
.sheet(isPresented: $showComputedTaskSheet) {
// self.selectedTask is returns nil
showScreen(task: self.selectedTask!)
}
.onAppear {
UITableView.appearance().backgroundColor = .white
}
}
Since I have no access to your full project this example can help you to get the idea, you can use .sheet() with item initializer like aheze said.
The advantage is here you pass optional to input item and you receive unwrapped safe value to work!
struct ContentView: View {
#State private var customValue: CustomValue?
var body: some View {
Button("Show the Sheet View") { customValue = CustomValue(description: "Hello, World!") }
.sheet(item: $customValue){ item in sheetView(item: item) }
}
func sheetView(item: CustomValue) -> some View {
return VStack {
Text(item.description)
Button("Close the Sheet View") { customValue = nil }.padding()
}
}
}
struct CustomValue: Identifiable {
let id: UUID = UUID()
var description: String
}
I'm building an app which makes graphs and calculates some basic stuff. On the main screen, I have a ForEach loop inside a List that shows the saved charts. When entering the NavigationLink inside the List, the destination view does not correspond with the label shown (video below).
I had to use a custom ForEach extension to deal with the bindings https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/ (Apple announced that on iOS 15 ForEach will accept bindings, but I'm developing for iOS 14).
The code of the List:
List{
ForEach($chartData.calibrations) { index, data in
VStack{
NavigationLink(
destination: DetailedChartView(index: index, currentData: data, isDetailShown: $detailVisible),
isActive: $detailVisible,
label: {
SavedListItem(index: index, savedData: self.chartData.calibrations[index])
})
.navigationBarHidden(true)
}
}.onDelete(perform: removeRows)
}.id(UUID())
.listStyle(PlainListStyle())
The code of the label (SavedListItem)
struct SavedListItem: View {
var index: Int
var data: ChartDataObject
var formattedSlope = ""
var formattedOrigin = ""
var formattedCoef = ""
init(index: Int, savedData: ChartDataObject) {
self.data = savedData
self.index = index
self.formattedSlope = String(format: "%.2f", data.slope)
self.formattedOrigin = String(format: "%.2f", abs(data.origin))
self.formattedCoef = String(format: "%.3f", data.regressionCoef)
}
var body: some View {
HStack {
Text("\(index + 1)").bold()
VStack(alignment: .leading,spacing: 10) {
Text("\(data.name)").font(.title3)
Text("\(formatDate(data.date))")
}.padding()
Spacer()
if data.origin > 0 {
VStack {
VStack {
Text("y = \(self.formattedSlope)x + \(formattedOrigin)")
Spacer().frame(height: 10)
Text("R2 = \(formattedCoef)")
}
}
}
else if data.origin == 0 {
VStack {
VStack {
Text("y = \(formattedSlope)x")
Spacer().frame(height: 10)
Text("R2 = \(formattedCoef)")
}
}
}
else if data.origin < 0 {
VStack {
VStack {
Text("y = \(formattedSlope)x - \(formattedOrigin)")
Spacer().frame(height: 10)
Text("R2 = \(formattedCoef)")
}
}
}
else {
Text("There was an error")
}
}
.padding()
}
private func formatDate(_ date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd-MM-yyyy"
let dateString = dateFormatter.string(from: date)
return dateString
}
}
And the code of DetailedChartView:
struct DetailedChartView: View {
#EnvironmentObject var calibrationsController: SavedCalibrationsController
var index: Int
#Binding var currentData: ChartDataObject
var copyData: ChartDataObject {
return currentData
}
#State var isEditing: Bool = false
#Binding var isDetailShown: Bool
var body: some View {
NavigationView{
VStack(spacing: 10) {
Text("y = \(currentData.origin) + \(currentData.slope)x")
Text("Slope: \(currentData.slope)")
Text("Origin: \(currentData.origin)")
Text("R2: \(currentData.regressionCoef)")
Divider()
RegressionChart(data: currentData).frame(height: 500)
}.navigationBarHidden(true)
}
.navigationBarTitle(currentData.name)
.toolbar(content: {
Button(action: {
self.isEditing = true
}, label: {
Text("Edit")
})
})
.sheet(isPresented: $isEditing, onDismiss: {
currentData = copyData
calibrationsController.saveData()
isEditing = false
isDetailShown = false
}, content: {
EditView(calibrationsController: calibrationsController, editVisible: $isEditing, isDetailShown: $isDetailShown, index: index, data: $currentData).environmentObject(calibrationsController)
})
}
}
When there is only one item the List behaves as expected.
This issue is driving me nuts: if I use a List (without the ForEach), everything works as expected but I cant change the ForEach because I lose the .onDelete() functionality, and deleting the items inside the detailed view (which has an edit button), gives me an index out of range error (another story...).
Sorry for the long post!
EDIT: Minimal reproductible example
https://drive.google.com/file/d/1WN_tGR_kVNVNqEOW054d6kMBlq8HGkF5/view?usp=sharing
remove
isActive: $detailVisible,
in MainList NavigationLink.
I'm working on my project with the feature of select multiple blocks of thumbnails. Only selected thumbnail(s)/image will be highlighted.
For the ChildView, The binding activeBlock should be turned true/false if a use taps on the image.
However, when I select a thumbnail, all thumbnails will be highlighted.I have come up with some ideas like
#State var selectedBlocks:[Bool]
// which should contain wether or not a certain block is selected.
But I don't know how to implement it.
Here are my codes:
ChildView
#Binding var activeBlock:Bool
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color("orange"))
}
}
}
BlockBView
struct VideoData: Identifiable{
var id = UUID()
var thumbnails: String
}
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "test"), VideoData(thumbnails: "test2"), VideoData(thumbnails: "test1")
]
#State var activeBlock = false
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(0..<videos.count) { _ in
Button(action: {
self.activeBlock.toggle()
}, label: {
ChildView(activeBlock: $activeBlock, thumbnail: "test")
})
}
}
}
}
Thank you for your help!
Here is a demo of possible approach - we initialize array of Bool by videos count and pass activated flag by index into child view.
Tested with Xcode 12.1 / iOS 14.1 (with some replicated code)
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "flag-1"), VideoData(thumbnails: "flag-2"), VideoData(thumbnails: "flag-3")
]
#State private var activeBlocks: [Bool] // << declare
init() {
// initialize state with needed count of bools
self._activeBlocks = State(initialValue: Array(repeating: false, count: videos.count))
}
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(videos.indices, id: \.self) { i in
Button(action: {
self.activeBlocks[i].toggle() // << here !!
}, label: {
ChildView(activeBlock: activeBlocks[i], // << here !!
thumbnail: videos[i].thumbnails)
})
}
}
}
}
}
struct ChildView: View {
var activeBlock:Bool // << value, no binding needed
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color.orange)
}
}
}
}
}
Final result
Build your element and it's model first. I'm using MVVM,
class RowModel : ObservableObject, Identifiable {
#Published var isSelected = false
#Published var thumnailIcon: String
#Published var name: String
var id : String
var cancellables = Set<AnyCancellable>()
init(id: String, name: String, icon: String) {
self.id = id
self.name = name
self.thumnailIcon = icon
}
}
//Equivalent to your BlockView
struct Row : View {
#ObservedObject var model: RowModel
var body: some View {
GroupBox(label:
Label(model.name, systemImage: model.thumnailIcon)
.foregroundColor(model.isSelected ? Color.orange : .gray)
) {
HStack {
Capsule()
.fill(model.isSelected ? Color.orange : .gray)
.onTapGesture {
model.isSelected = !model.isSelected
}
//Two way binding
Toggle("", isOn: $model.isSelected)
}
}.animation(.spring())
}
}
Prepare data and handle action in your parent view
struct ContentView: View {
private let layout = [GridItem(.flexible()),GridItem(.flexible())]
#ObservedObject var model = ContentModel()
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(model.rowModels) { model in
Row(model: model)
}
}
}
if model.selected.count > 0 {
HStack {
Text(model.selected.joined(separator: ", "))
Spacer()
Button(action: {
model.clearSelection()
}, label: {
Text("Clear")
})
}
}
}
.padding()
.onAppear(perform: prepare)
}
func prepare() {
model.prepare()
}
}
class ContentModel: ObservableObject {
#Published var rowModels = [RowModel]()
//I'm handling by ID for futher use
//But you can convert to your Array of Boolean
#Published var selected = Set<String>()
func prepare() {
for i in 0..<20 {
let row = RowModel(id: "\(i)", name: "Block \(i)", icon: "heart.fill")
row.$isSelected
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] selected in
guard let `self` = self else { return }
print(selected)
if selected {
self.selected.insert(row.name)
}else{
self.selected.remove(row.name)
}
}).store(in: &row.cancellables)
rowModels.append(row)
}
}
func clearSelection() {
for r in rowModels {
r.isSelected = false
}
}
}
Don't forget to import Combine framework.