Change (toggle) property on passed object [closed] - swiftui

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 1 year ago.
Improve this question
I'm clearly not understanding this in SwiftUI.
I have a task struct with several properties one of which - 'isHotList' is a Boolean that I want to be able to toggle on and off.
I created a row view for each task and then use this to populate a list view for all the tasks loaded from JSON - no problems there. Moved on and added a detail view (which takes a task object passed in from the list view) with a toggle button for the isHotList the problem is that the toggle works correctly in the detail view but doesn't carry back to the list view .... ?
I suspect I've created two seperate instances of the task but I'm not sure. I've used #State to declare the task but I can't get #Binding to work correctly for me.
Toggle in the detail view, task is declared with #State in this view file ...
Button(action: {
task.isHotList.toggle()
print(task.isHotList)
}){
Image(systemName: "flame.fill")
.foregroundColor(task.isHotList ? Color.orange : Color.gray)
}
The list view (separate file)
ForEach(filteredHotTasks) { task in
NavigationLink(destination: TaskDetail(task: task)) {
TaskRow(task: task)
}
}
I'm really trying to learn here but this has me confused as to what is / isn't going on
As requested the full detail view file
struct TaskDetail: View {
#EnvironmentObject var modelData: ModelData
#State var task: Task
var taskIndex: Int {
modelData.tasks.firstIndex(where: {$0.id == task.id})!
}
var body: some View {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(task.title)
.font(.title2)
.padding(.bottom, 10)
Spacer()
Button(action: {
task.isHotList.toggle()
print(task.isHotList)
}){
Image(systemName: "flame.fill")
.foregroundColor(task.isHotList ? Color.orange : Color.gray)
}
}
HStack {
Text(task.category)
.font(.subheadline)
Spacer()
HStack {
Image(systemName: "calendar.badge.clock")
Text(task.dueDate)
.font(.subheadline)
}
}
}
.padding()
}
}
}

Remove #State
struct TaskDetail: View {
#EnvironmentObject var modelData: ModelData
var task: Task /* NEW */
var taskIndex: Int {
modelData.tasks.firstIndex(where: {$0.id == task.id})!
}
}
And use
Button(action: {
/* filteredHotTasks is the array that contains tasks. */
modelData.filteredHotTasks[taskIndex].isHotList.toggle()
print(task.isHotList)
})
Remember to pass modelData using .environmentObject(modelData) somwhere in one of your root views.
If you do not do, use #ObservedObject instead of #EnvironmentObject.
struct TaskDetail: View {
#ObservedObject var modelData: ModelData /* NEW */
var task: Task /* NEW */
var taskIndex: Int {
modelData.tasks.firstIndex(where: {$0.id == task.id})!
}
}
And
NavigationLink(destination: TaskDetail(modelData: modelData, task: task)) {
TaskRow(task: task)
}
Have a look at my answer to this question
Declaring TextField and Toggle in a SwiftUI dynamic form
For more info, read this article.
Managing Model Data in Your App
🎁
Confirm Task to Equatable in addition to the rest of the protocols you confirm to.
struct Task: Codable, Identifiable, Equatable {
}
Doing so you avoid using task.id whenever you want to know if two tasks are equal as in the firstIndex
var taskIndex: Int {
modelData.tasks.firstIndex(where: {$0 == task})!
}

Related

Get selected item in SwiftUI list without using a navigation link

I'm writing a SwiftUI Mac app that is similar to a kanban board. The app has three lists: Todo, Doing, and Done. At the bottom of each list is a button to move a task to another list. For example the todo list has a Start Doing button. Selecting a task from the todo list and clicking the button should move the task from the todo list to the doing list.
Every SwiftUI list selection example I have seen uses a navigation link. Selecting a list item takes you to another view. But I don't want to want to navigate to another view when selecting a list item. I want the selected task so I can change its status and move it to the correct list when clicking the button.
Here's the code for one of my lists.
struct TodoList: View {
// The board has an array of tasks.
#Binding var board: KanbanBoard
#State private var selection: Task? = nil
#State private var showAddSheet = false
var body: some View {
VStack {
Text("Todo")
.font(.title)
List(todoTasks, selection: $selection) { task in
Text(task.title)
}
HStack {
Button(action: { showAddSheet = true }, label: {
Label("Add", systemImage: "plus.square")
})
Spacer()
Button(action: { selection?.status = .doing}, label: {
Label("Start Doing", systemImage: "play.circle")
})
}
}
.sheet(isPresented: $showAddSheet) {
AddTaskView(board: $board)
}
}
var todoTasks: [Task] {
// Task conforms to Identifiable.
// A task has a status that is an enum: todo, doing, or done.
return board.tasks.filter { $0.status == .todo}
}
}
When I click on a list item, it is not selected.
How do I get the selected item from the list without using a navigation link?
Workaround
Tamas Sengel's answer led me to a workaround. Give each list item a Start Doing button so I don't have to track the selection.
List(todoTasks, id: \.self) { task in
HStack {
Text(task.title)
Button {
task.status = .doing
} label: {
Text("Start Doing")
}
}
}
The workaround helps for my specific case. But I'm going to keep the question open in hopes of an answer that provides a better alternative to using a button for people who want a way to get the selected list item.
Use a Button in the List and in the action, set a #State variable to the current list item.
#State var currentTask: Task?
List(todoTasks, id: \.self) { task in
Button {
currentTask = task
} label: {
Text(task.title)
}
}
Use .environment(\.editMode, .constant(.active)) to turn on selecting capability.
import SwiftUI
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
let id = UUID()
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var multiSelection = Set<UUID>()
var body: some View {
NavigationView {
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.environment(\.editMode, .constant(.active))
.onTapGesture {
// This is a walk-around: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
print(multiSelection)
})
}
}
Text("\(multiSelection.count) selections")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Put your 3 List with same data array but filtering by status on each one something like:
task.filter({ $0.status == .toDo })
Then on your row add the modifier .onTapGesture be sure to cover all the available space.
Inside the code block introduce your logic or func to change the item status. changeTaskStatus(item: task)

SwiftUI - How to pass data then initialise and edit data

I'm downloading data from Firebase and trying to edit it. It works, but with an issue. I am currently passing data to my EditViewModel with the .onAppear() method of my view. And reading the data from EditViewModel within my view.
class EditViewModel: ObservableObject {
#Published var title: String = ""
}
struct EditView: View {
#State var selected_item: ItemModel
#StateObject var editViewModel = EditViewModel()
var body: some View {
VStack {
TextField("Name of item", text: self.$editViewModel.title)
Divider()
}.onAppear {
DispatchQueue.main.async {
editViewModel.title = selected_item.title
}
}
}
}
I have given you the extremely short-hand version as it's much easier to follow.
However, I push to another view to select options from a list and pop back. As a result, everything is reset due to using the onAppear method. I have spent hours trying to use init() but I am struggling to get my application to even compile, getting errors in the process. I understand it's due to using the .onAppear method, but how can I use init() for this particular view/view-model?
I've search online but I've found the answers to not be useful, or different from what I wish to achieve.
Thank you.
You don't need to use State for input property - it is only for internal view usage. So as far as I understood your scenario, here is a possible solution:
struct EditView: View {
private var selected_item: ItemModel
#StateObject var editViewModel = EditViewModel()
init(selectedItem: ItemModel) {
selected_item = selectedItem
editViewModel.title = selectedItem.title
}
var body: some View {
VStack {
TextField("Name of item", text: self.$editViewModel.title)
Divider()
}.onAppear {
DispatchQueue.main.async {
editViewModel.title = selected_item.title
}
}
}
}

How to make a reusable TextPopUp in SwiftUI?

I am having trouble building a reusable SwiftUI text pop up view. I think the problem is the binding but I not sure. It is supposed to be a ContextMenu, but the reason I am not using ContextMenu is it does not show enough lines of text.
So far I have this...
struct TextPopUpView: View {
#EnvironmentObject var oracleViewModel: OracleViewModel
#Binding var showPopover: Bool
var displayedText: String
var popUpText: String
var body: some View {
Text("\(displayedText)")
.font(.title)
.fontWeight(.bold)
.onLongPressGesture {
self.showPopover = true
}
.popover(isPresented: $showPopover) {
Text("\(self.popUpText)")
.frame(width: 250.0)
.onTapGesture {
self.showPopover = false
}
}
}
}
And than I implement it for a particular view like so...
TextPopUpView(showPopover: $showPopover, displayedText: oracleViewModel.***someElement***, popUpText: oracleViewModel.getDescriptionFor***SomeElement***()).environmentObject(oracleViewModel)
where 'someElement' is the particular element text and popup text I want to show.
The problem is when I use TextPopUpView more than once in a view, the Popup view only displays the text for the last implementation on the said page.
I am guessing I am doing something wrong in the implementations of a reusable view, but I am not sure what. Any suggestions?
EDIT: (Example Used in code)
struct TopView: View {
#EnvironmentObject var oracleViewModel: OracleViewModel
#State private var showPopover: Bool = false
***Blarg Blarg Blarg***
VStack{
VStack {
Text("RUNE")
.font(.system(size:10))
.padding(.bottom, 5.0)
TextPopUpView(showPopover: $showPopover,
displayedText: oracleViewModel.rune,
popUpText: oracleViewModel.getDescriptionForRune()).environmentObject(oracleViewModel)
}
.padding(.bottom)
VStack {
Text("ELEMENT")
.font(.system(size:10))
.padding(.bottom, 5.0)
TextPopUpView(showPopover: $showPopover,
displayedText: oracleViewModel.element,
popUpText: oracleViewModel.getDescriptionForElement())
.environmentObject(oracleViewModel)
}
}
***Blarg Blarg Blarg***
}
(For some reason I only get the description for the Element, when I long press either view the TextPopUp is attached to)

SwiftUI: MasterDetailView within in Tabbed View loses "State"

I have a MasterDetailView within a Tabbed View. If the user tabs the MasterDetailView and selects an entry in the master view, the detail is presented in the detail view. After selecting another tab and switching back to the MasterDetailView, the detail is no longer selected - the MasterDetailView completely loses its state like is is completely rendered.
private let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .medium
return dateFormatter
}()
struct MasterDetailView: View {
#State private var dates = [Date]()
var body: some View {
NavigationView {
MasterView(dates: $dates)
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton(),
trailing: Button(
action: {
withAnimation { self.dates.insert(Date(), at: 0) }
}
) {
Image(systemName: "plus")
}
)
DetailView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
struct MasterView: View {
#Binding var dates: [Date]
var body: some View {
List {
ForEach(dates, id: \.self) { date in
NavigationLink(
destination: DetailView(selectedDate: date)
) {
Text("\(date, formatter: dateFormatter)")
}
}.onDelete { indices in
indices.forEach { self.dates.remove(at: $0) }
}
}
}
}
struct DetailView: View {
var selectedDate: Date?
var body: some View {
Group {
if selectedDate != nil {
Text("\(selectedDate!, formatter: dateFormatter)")
} else {
Text("Detail view content goes here")
}
}.navigationBarTitle(Text("Detail"))
}
}
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection){
Text("First View")
.font(.title)
.tabItem {
VStack {
Image("first")
Text("First")
}
}
.tag(0)
MasterDetailView()
.tabItem {
VStack {
Image("second")
Text("Master Detail")
}
}
.tag(1)
}
}
}
Is there a way to "reuse" the MasterDetailView when the user selects that tab?
I know I can use #State and #Binding to save and restore the state (like the selected entry in the master view) and in that simple example that might be a solution. But in a complex app - for example when the MasterDetailView includes a deep view hierarchy - it's not useful to manage (save and restore) the complete state of a view.
Variations on this question have been asked several times, and so far, the consensus is that SwiftUI does not support this use case yet. I'm sure it's on their radar, but the more people who ask for this feature, the more likely it will be prioritized for next year's updates.
Here's an answer where someone got the behavior you're wanting by wrapping UITabBarController for use with SwiftUI.

Why does binding to the Picker not work anymore in swiftui?

When I run a Picker Code in the Simulator or the Canvas, the Picker goes always back to the first option with an animation or just freezes. This happens since last Thursday/Friday. So I checked some old simple code, where it worked before that and it doesn't work for me there, too.
This is the simple old Code. It doesn't work anymore in beta 3, 4 and 5.
struct PickerView : View {
#State var selectedOptionIndex = 0
var body: some View {
VStack {
Text("Option: \(selectedOptionIndex)")
Picker(selection: $selectedOptionIndex, label: Text("")) {
Text("Option 1")
Text("Option 2")
Text("Option 3")
}
}
}
}
In my newer code, I used #ObservedObject, but also here it doesn't work.
Also I don't get any errors and it builds and runs.
Thank you for any pointers.
----EDIT----- Please look at the answer first
After the help, that I could use the .tag() behind all Text()like Text("Option 1").tag(), it now takes the initial value and updates it inside the view. If I use #ObservedObject like here:
struct PickerView: View {
#ObservedObject var data: Model
let width: CGFloat
let height: CGFloat
var body: some View {
VStack(alignment: .leading) {
Picker(selection: $data.exercise, label: Text("select exercise")) {
ForEach(data.exercises, id: \.self) { exercise in
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise))
}
}
.frame(width: width, height: (height/2), alignment: .center)
}
}
}
}
Unfortunately it doesn't reflect changes on the value, if I make these changes in another view, one navigationlink further. And also it doesn't seem to work with the my code above, where I use firstIndex(of: exercise)
---EDIT---
Now the code above works if I change
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise))
into
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise)!)
because it couldn't work with an optional.
The answer summarized:
With the .tag() behind the Options it works. It would look like following:
Picker(selection: $selectedOptionIndex, label: Text("")) {
ForEach(1...3) { index in
Text("Option \(index)").tag(index)
}
}
If you use a range of Objects it could look like this:
Picker(selection: $data.exercises, label: Text("")) {
ForEach(0..<data.exercises.count) { index in
Text("\(data.exercises[index])").tag(index)
}
}
I am not sure if it is intended, that .tag() is needed to be used here, but it's at least a workaround.
I found a way to simplify the code a bit without the need of operating on indicies and tags.
At first, make sure to conform your model to Identifiable protocol like this (this is actually a key part, as it enables SwiftUI to differentiate elements):
public enum EditScheduleMode: String, CaseIterable, Identifiable {
case closeSchedule
case openSchedule
public var id: EditScheduleMode { self }
var localizedTitle: String { ... }
}
Then you can declare viewModel like this:
public class EditScheduleViewModel: ObservableObject {
#Published public var editScheduleMode = EditScheduleMode.closeSchedule
public let modes = EditScheduleMode.allCases
}
and UI:
struct ModeSelectionView: View {
private let elements: [EditScheduleMode]
#Binding private var selectedElement: EditScheduleMode
internal init?(elements: [EditScheduleMode],
selectedElement: Binding<EditScheduleMode>) {
self.elements = elements
_selectedElement = selectedElement
}
internal var body: some View {
VStack {
Picker("", selection: $selectedElement) {
ForEach(elements) { element in
Text(element.localizedTitle)
}
}
.pickerStyle(.segmented)
}
}
}
With all of those you can create a view like this:
ModeSelectionView(elements: viewModel.modes, selectedElement: $viewModel.editScheduleMode)