State is nil when showing sheet - ios

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
}

Related

How to update filtered list in swiftui, when the value in the filter is changed?

Usual caveat of being new to swiftui and apologies is this is a simple question.
I have a view where I have a date picker, as well as two arrows to increase/decrease the day. When this date is update, I am trying to filter a list of 'sessions' from the database which match the currently displayed date.
I have a filteredSessions variable which applies a filter to all 'sessions' from the database. However I do not seem to have that filter refreshed each time the date is changed.
I have the date to be used stored as a "#State" object in the view. I thought this would trigger the view to update whenever that field is changed? However I have run the debugger and found the 'filteredSessions' variable is only called once, and not when the date is changed (either by the picker or the buttons).
Is there something I'm missing here? Do I need a special way to 'bind' this date value to the list because it isn't directly used by the display?
Code below. Thanks
import SwiftUI
struct TrainingSessionListView: View {
#StateObject var viewModel = TrainingSessionsViewModel()
#State private var displayDate: Date = Date.now
#State private var presentAddSessionSheet = false
private var dateManager = DateManager()
private let oneDay : Double = 86400
private var addButton : some View {
Button(action: { self.presentAddSessionSheet.toggle() }) {
Image(systemName: "plus")
}
}
private var decreaseDayButton : some View {
Button(action: { self.decreaseDay() }) {
Image(systemName: "chevron.left")
}
}
private var increaseDayButton : some View {
Button(action: { self.increaseDay() }) {
Image(systemName: "chevron.right")
}
}
private func sessionListItem(session: TrainingSession) -> some View {
NavigationLink(destination: TrainingSessionDetailView(session: session)) {
VStack(alignment: .leading) {
Text(session.title)
.bold()
Text("\(session.startTime) - \(session.endTime)")
}
}
}
private func increaseDay() {
self.displayDate.addTimeInterval(oneDay)
}
private func decreaseDay() {
self.displayDate.addTimeInterval(-oneDay)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
decreaseDayButton
Spacer()
DatePicker("", selection: $displayDate, displayedComponents: .date)
.labelsHidden()
Spacer()
increaseDayButton
Spacer()
}
.padding(EdgeInsets(top: 25, leading: 0, bottom: 0, trailing: 0))
Spacer()
ForEach(filteredSessions) { session in
sessionListItem(session: session)
}
Spacer()
}
.navigationTitle("Training Sessions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: addButton)
.sheet(isPresented: $presentAddSessionSheet) {
TrainingSessionEditView()
}
}
}
var filteredSessions : [TrainingSession] {
print("filteredSessions called")
return viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }
}
}
struct TrainingSessionListView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionListView()
}
}
There are two approaches and for your case and for what you described I would take the first one. I only use the second approach if I have more complex filters and tasks
You can directly set the filter on the ForEach this will ensure it gets updated whenever the displayDate changes.
ForEach(viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }) { session in
sessionListItem(session: session)
}
Or you can like CouchDeveloper said, introduce a new state variable and to trigger a State change you would use the willSet extension (doesn't exist in binding but you can create it)
For this second option you could do something like this.
Start create the Binding extension for the didSet and willSet
extension Binding {
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
let snapshot = self.wrappedValue
self.wrappedValue = $0
execute(snapshot)
}
)
}
func willSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute($0)
self.wrappedValue = $0
}
)
}
}
Introduce the new state variable
#State var filteredSessions: [TrainingSession] = []
// removing the other var
We introduce the function that will update the State var
func filterSessions(_ filter: Date) {
filteredSessions = viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: date) }
}
We update the DatePicker to run the function using the willSet
DatePicker("", selection: $displayDate.willSet { self.filterSessions($0) }, displayedComponents: .date)
And lastly we add a onAppear so we fill the filteredSessions immidiatly (if you want)
.onAppear { filterSessions(displayDate) } // uses the displayDate that you set as initial value
Don't forget in your increaseDay() and decreaseDay() functions to add the following after the addTimeInterval
self.filterSessions(displayDate)
As I said, this second method might be better for more complex filters
Thank you all for your responses. I'm not sure what the issue was originally but it seems updating my view to use Firebase's #FirestoreQuery to access the collection updates the var filteredSessions... much better than what I had before.
New code below seems to be working nicely now.
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionListView: View {
#FirestoreQuery(collectionPath: "training_sessions") var sessions : [TrainingSession]
#State private var displayDate: Date = Date.now
#State private var presentAddSessionSheet = false
private var dateManager = DateManager()
private let oneDay : Double = 86400
private var addButton : some View {
Button(action: { self.presentAddSessionSheet.toggle() }) {
Image(systemName: "plus")
}
}
private var todayButton : some View {
Button(action: { self.displayDate = Date.now }) {
Text("Today")
}
}
private var decreaseDayButton : some View {
Button(action: { self.decreaseDay() }) {
Image(systemName: "chevron.left")
}
}
private var increaseDayButton : some View {
Button(action: { self.increaseDay() }) {
Image(systemName: "chevron.right")
}
}
private func sessionListItem(session: TrainingSession) -> some View {
NavigationLink(destination: TrainingSessionDetailView(sessionId: session.id!)) {
VStack(alignment: .leading) {
Text(session.title)
.bold()
Text("\(session.startTime) - \(session.endTime)")
}
}
}
private func increaseDay() {
self.displayDate.addTimeInterval(oneDay)
}
private func decreaseDay() {
self.displayDate.addTimeInterval(-oneDay)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
decreaseDayButton
Spacer()
DatePicker("", selection: $displayDate, displayedComponents: .date)
.labelsHidden()
Spacer()
increaseDayButton
Spacer()
}
.padding(EdgeInsets(top: 25, leading: 0, bottom: 10, trailing: 0))
if filteredSessions.isEmpty {
Spacer()
Text("No Training Sessions found")
} else {
List {
ForEach(filteredSessions) { session in
sessionListItem(session: session)
}
}
}
Spacer()
}
.navigationTitle("Training Sessions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: todayButton, trailing: addButton)
.sheet(isPresented: $presentAddSessionSheet) {
TrainingSessionEditView()
}
}
}
var filteredSessions : [TrainingSession] {
return sessions.filter { $0.date == dateManager.dateToStr(date: displayDate)}
}
}
struct TrainingSessionListView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionListView()
}
}

Why fullScreenCover always take first index from array?

Why fullScreenCover always take just first index of an array?
This is some example of code:
struct TestView: View {
#State private var isFullScreen: Bool = false
var body: some View {
VStack{
ForEach(0..<5, id:\.self) { number in
VStack{
Text("\(number)")
.background(.red)
.foregroundColor(.white)
.padding(20)
.onTapGesture {
isFullScreen.toggle()
}
}
.fullScreenCover(isPresented: $isFullScreen) {
test2View(title: number)
}
}
}
}
}
This is the code of test2View:
struct test2View: View {
var title:Int
var body: some View {
Text("\(title)")
}
}
Whenever I click on any number it always show just 0, but when I make navigationLink instead of fullScreenCover, it works as expected, but navigationLink isn't a solution for my problem, I want that to be fullScreenCover.
It's because fullScreenCover is using a single isFullScreen for each number so only the first one works. Fix by adding a third intermediary View to hold an isFullScreen bool for each number, e.g.
struct TestView: View {
var body: some View {
VStack{
ForEach(0..<5) { number in
TestView2(number: number)
}
}
}
}
struct TestView2: View {
let number: Int
#State private var isFullScreen: Bool = false
var body: some View {
Text("\(number, format: .number)")
.background(.red)
.foregroundColor(.white)
.padding(20)
.onTapGesture {
isFullScreen.toggle()
}
.fullScreenCover(isPresented: $isFullScreen) {
TestView3(number: number)
}
}
}
struct TestView3: View {
let number: Int
var body: some View {
Text("\(number, format: .number)")
}
}
I found a solution using .fullScreenCover item parameter like this:
struct TestView: View {
#State private var isFullScreen: Int? = nil
var body: some View {
VStack{
ForEach(0..<5, id:\.self) { number in
VStack{
Text("\(number)")
.background(.red)
.foregroundColor(.white)
.padding(20)
.onTapGesture {
isFullScreen = number
}
}
.fullScreenCover(item: $isFullScreen) { item in
test2View(title: item)
}
}
}
}
}

Why don't #State parameter changes cause a view update?

I am trying to follow the guidance in a WWDC video to use a #State struct to configure and present a child view. I would expect the struct to be able to present the view, however the config.show boolean value does not get updated when set by the button.
The code below has two buttons, each toggling a different boolean to show an overlay. Toggling showWelcome shows the overlay but toggling config.show does nothing. This seems to be working as intended, I just don't understand why SwiftUI behaves this way. Can someone explain why it's not functioning like I expect, and/or suggest a workaround?
https://developer.apple.com/videos/play/wwdc2020/10040/ # 5:14
struct InformationOverlayConfig {
#State var show = false
#State var title: String?
}
struct InformationOverlay: View {
#Binding var config: InformationOverlayConfig
var body: some View {
if config.title != nil {
Text(config.title!)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
}
}
struct TestView: View {
#State private var configWelcome = InformationOverlayConfig(title: "Title is here")
#State private var showWelcome = false
var body: some View {
VStack {
Text("hello world")
Spacer()
Button("Toggle struct parameter", action: {
configWelcome.show.toggle()
})
Button("Toggle boolean state", action: {
showWelcome.toggle()
})
}
.overlay(
VStack {
InformationOverlay(config: $configWelcome).opacity(configWelcome.show ? 1 : 0)
InformationOverlay(config: $configWelcome).opacity(showWelcome ? 1 : 0)
})
}
You "Config" is not a View. State variables only go in Views.
Also, do not use force unwrapping for config.title. Optional binding or map are the non-redundant solutions.
Lastly, there is no need for config to be a Binding if it actually functions as a constant for a particular view.
struct InformationOverlay: View {
struct Config {
var show = false
var title: String?
}
let config: Config
var body: some View {
VStack {
if let title = config.title {
Text(title)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
// or
config.title.map {
Text($0)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
}
}
}
struct TestView: View {
#State private var configWelcome = InformationOverlay.Config(title: "Title is here")
#State private var showWelcome = false
var body: some View {
VStack {
Text("hello world")
Spacer()
Button("Toggle struct parameter") {
configWelcome.show.toggle()
}
Button("Toggle boolean state") {
showWelcome.toggle()
}
}
.overlay(
VStack {
InformationOverlay(config: configWelcome).opacity(configWelcome.show ? 1 : 0)
InformationOverlay(config: configWelcome).opacity(showWelcome ? 1 : 0)
}
)
}
}

Why is withAnimation changing how these references are managed?

In the following code, including one less line in a withAnimation block triggers an unowned reference fault. Dismissing the view shown by button A and B should trigger the addition of a new element in both case. However, B causes the reference to the items list to be invalid:
struct ListItem: Identifiable, Equatable {
var id = UUID()
var name: String
}
class MyListClass: ObservableObject {
#Published var items = [ListItem]()
}
struct DetailView: View {
#Binding var item: ListItem
var body: some View {
VStack(alignment: .leading) {
TextField("Name", text: self.$item.name)
}
}
}
struct MyListView: View {
#StateObject var myList = MyListClass()
#State private var itemDraft = ListItem(name: "Foo")
#State private var showingA = false
#State private var showingB = false
var body: some View {
NavigationView {
List(myList.items) { item in
// Pass binding to item into DetailsView
Text(item.name)
}
.navigationBarItems(
leading: Button(action: {
showingA.toggle()
}) {
Text("A")
},
trailing: Button(action: {
showingB.toggle()
}) {
Text("B")
}
)
.sheet(isPresented: $showingA) {
DetailView(item: $itemDraft)
.onDisappear { // Closure A
let newItem = itemDraft
withAnimation {
itemDraft = ListItem(name: "Bar")
myList.items.append(newItem) // <- here
}
}
}
.sheet(isPresented: $showingB) {
DetailView(item: $itemDraft)
.onDisappear { // Closure B
let newItem = itemDraft
itemDraft = ListItem(name: "Bar")
withAnimation {
myList.items.append(newItem) // <- and here
}
}
}
}
}
}
The error is
Attempted to read an unowned reference but the object was already deallocated
What is the difference in references management between closures A (annotation above) and B? (Bonus points: what tool of xcode is the most useful to find the source of that bug?)

SwiftUI: How to select multi items(image) with ForEach?

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.

Resources