How to put checkmark when Menu item selected SwiftUI - swiftui

Im trying to put checkmark when menu item selected.I tried give a #State var selectedSort : Int = .zero and give id(selectedSort) but It didn't work. How can I solve this problem ?
This is my code;
struct SortMenuPickerView : View {
#Binding var sortClicked : Bool
#ObservedObject var productListViewModel = ProductListViewModel()
#State var sortListArray : [ProductListSortAndFilterList]
var function: () -> Void
#Binding var sortId : String
var body : some View {
HStack(alignment: .center){
Spacer()
Menu {
ForEach(sortListArray,id:\.id){ item in
if item.id == "sort" {
ForEach(item.sortList ?? [],id:\.id) { data in
Button(action: {
sortId = (data.id ?? "")
self.function()
print("selected item is : \(data.id!)")
}) {
Text(data.name ?? "")
.tag(data.id)
}
}
}
}
} label: {
SortView()
}

Try with picker style menu & can customise with your requirements...
Its sample example:
#State private var sort: Int = 0
var body: some View {
NavigationView {
Text("Hello World!")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Menu {
Picker(selection: $sort, label:Text("Sortingoptions")) {
Text("Sort1").tag(0)
Text("Sort2").tag(1)
Text("Sort3").tag(2)
}
}
label: {
Text("Sort")
}
}
}
}
}

Related

#State var with array does not update view when grandchild updates it

I have 3 views. The issue I have is that the 1st one (a parent view of 2) is not changing when the 3rd one (a children of view 2) has updated a property in the array.
Let's use some code so it is easier to understand:
public struct Item {
public let id: String
public var name: String?
public var inStock: Bool
import SwiftUI
#main
struct ThisIsMessedUpApp: App {
var body: some Scene {
WindowGroup {
ItemsMainView()
}
}
}
import Foundation
import SwiftUI
struct ItemsMainView: View {
#State var items = [Item]()
var body: some View {
VStack {
Text("Item count is \(items.count)")
Divider()
VStack {
Text("ItemsMainView has:")
HStack {
ForEach(self.items, id: \.id) { item in
Text(item.name ?? "nothing found")
Spacer()
Text(item.inStock.description)
}
}
}
ItemsView(items: $items)
}
}
}
struct ItemsView: View {
#Binding var items: [Item]
var body: some View {
Button("Add new item (call made from ItemsView", action: {
self.items.append(Item(id: UUID().uuidString,
name: "Test #1",
inStock: false))
})
VStack {
ForEach($items, id: \.id) { $item in
ItemView(item: $item)
}
}
}
}
struct ItemView: View {
#Binding var item: Item
#State var draftItemName: String = ""
var body: some View {
Text("ItemView has")
HStack {
TextField("TextField", text: $draftItemName)
.onSubmit {
item.name = draftItemName
}
Spacer()
Text(item.inStock.description)
}
.onAppear {
draftItemName = item.name ?? ""
}
}
}
Some of the Text are for debugging purposes.
If you run this code and change the second TextField's value to, say, "Test #2", you will see that you end up with an inconsistent UI state: ItemsMainView has "Test #1", whereas ItemView has "Test #2"
In Xcode 14.2, create a new project for ios or macos. Copy and paste the following code, replacing
the original ContentView.
Tell us if this code, tested on real ios 16.3 devices (not Previews), macCatalyst and MacOS 13.2 only, works for you.
struct ContentView: View {
var body: some View {
ItemsMainView()
}
}
public struct Item {
public let id: String
public var name: String?
public var inStock: Bool
}
struct ItemsMainView: View {
#State var items = [Item]()
var body: some View {
VStack {
Text("Item count is \(items.count)")
Divider()
VStack {
Text("ItemsMainView has:")
ForEach(items, id: \.id) { item in
HStack {
Text(item.name ?? "nothing found").foregroundColor(.red)
Spacer()
Text(item.inStock.description).foregroundColor(.red)
}
}
}
ItemsView(items: $items)
}
}
}
struct ItemsView: View {
#Binding var items: [Item]
var body: some View {
Button("Add new item (call made from ItemsView", action: {
self.items.append(Item(id: UUID().uuidString, name: "Test #1", inStock: false))
}).buttonStyle(.bordered)
VStack {
ForEach($items, id: \.id) { $item in
ItemView(item: $item)
}
}
}
}
struct ItemView: View {
#Binding var item: Item
#State var draftItemName: String = ""
var body: some View {
Text("ItemView has")
HStack {
TextField("TextField", text: $draftItemName)
.onSubmit {
item.name = draftItemName
}
Spacer()
Text(item.inStock.description)
}
.onAppear {
draftItemName = item.name ?? ""
}
}
}

#EnvironmentObject bug in SwiftUI

When I press the Move button in the contextMenu, I change the isCopied and setOriginPath variables in the EnvironmentObject. When this change is made, the List view is cleared and I can't see anything on the screen. I don't have any problems when I don't use EnvironmentObject.
ContextMenu:
.contextMenu {
Button {
safeFileVM.hideSelectedFile(fileName: currentFile.fileName)
safeFileVM.takeArrayOfItems()
} label: {
HStack {
Text(!currentFile.isLock ? "Hide" : "Show")
Image(systemName: currentFile.isLock ? "eye" : "eye.slash")
}
}
Button {
safeFileClipboard.setOriginPath = URL(fileURLWithPath: currentFile.localPath)
safeFileClipboard.isCopied = true
} label: {
HStack {
Text("Move")
Image(systemName: "arrow.up.doc")
}
}
}
View:
struct DetailObjectView: View {
#ObservedObject var safeFileVM: SafeFileViewModel = SafeFileViewModel()
#EnvironmentObject var safeFileClipboard: SafeFileClipBoard
var currentFile: MyFile
var currentLocation = ""
var body: some View {
VStack {
.....
}
.contextMenu {
Button {
safeFileVM.hideSelectedFile(fileName: currentFile.fileName)
safeFileVM.takeArrayOfItems()
} label: {
HStack {
Text(!currentFile.isLock ? "Hide" : "Show")
Image(systemName: currentFile.isLock ? "eye" : "eye.slash")
}
}
Button {
safeFileClipboard.setOriginPath = URL(fileURLWithPath: currentFile.localPath)
safeFileClipboard.isCopied = true
} label: {
HStack {
Text("Move")
Image(systemName: "arrow.up.doc")
}
}
}
}
}
In the mini project below, when the EnvironmentObject value changes, navigation goes to the beginning. Why ? How can I fix this ?
Example Project:
Main:
#main
struct EnvironmentTestApp: App {
#StateObject var fooConfig: FooConfig = FooConfig()
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
.environmentObject(fooConfig)
}
}
}
}
ContentView:
struct ContentView: View {
#EnvironmentObject var fooConfig: FooConfig
private let numbers: [Number] = [.init(item: "1"), .init(item: "2"), .init(item: "3")]
var body: some View {
List {
ForEach(numbers, id: \.id) { item in
DetailView(itemNumber: item.item)
}
}
}
}
struct Number: Identifiable {
var id = UUID()
var item: String
}
DetailView:
struct DetailView: View {
#EnvironmentObject var fooConfig: FooConfig
var itemNumber: String = ""
var body: some View {
NavigationLink(destination: ContentView().environmentObject(fooConfig)) {
Text("\(itemNumber) - \(fooConfig.fooBool == true ? "On" : "Off")")
.environmentObject(fooConfig)
.contextMenu {
Button {
fooConfig.fooBool.toggle()
} label: {
HStack {
Text(fooConfig.fooBool != true ? "On" : "Off")
}
}
}
}
}
}
ObservableObject:
class FooConfig: ObservableObject {
#Published var fooBool: Bool = false
}
Move that from scene into ContentView, because scene is a bad place to update view hierarchy, it is better to do inside view hierarchy, so here
struct EnvironmentTestApp: App {
var body: some Scene {
WindowGroup {
ContentView() // only root view here !!
}
}
}
everything else is inside views, like
struct ContentView: View {
#StateObject private var foo = FooConfig()
var body: some View {
NavigationView {
MainView()
.environmentObject(foo) // << here !!
}
}
}
struct MainView: View {
#EnvironmentObject var fooConfig: FooConfig
private let numbers: [Number] = [.init(item: "1"), .init(item: "2"), .init(item: "3")]
var body: some View {
List {
ForEach(numbers, id: \.id) { item in
DetailView(itemNumber: item.item)
}
}
}
}
and so on...
Tested with Xcode 14 / iOS 16

SwiftUI: button in ToolbarItem(placement: .principal) not work after change it's label

Xcode 12 beta 6
There is a button in toolbar, its label text is binding to a state var buttonTitle. I want to tap this button to trigger a sheet view, select to change the binding var.
After back to content view, the button's title is updated. But if you tap the button again, it not work.
Code:
struct ContentView: View {
#State var show = false
#State var buttonTitle = "button A"
var body: some View {
NavigationView {
Text("Hello World!")
.toolbar {
ToolbarItem(placement: .principal) {
Button {
show.toggle()
} label: {
Text(buttonTitle)
}
.sheet(isPresented: $show) {
SelectTitle(buttonTitle: $buttonTitle)
}
}
}
}
}
}
struct SelectTitle: View {
#Environment(\.presentationMode) var presentationMode
#Binding var buttonTitle: String
var body: some View {
Button("Button B") {
buttonTitle = "Button B"
presentationMode.wrappedValue.dismiss()
}
}
}
It is known toolbar-sheet layout issue, see also here. You can file another feedback to Apple.
Here is a workaround for your case - using callback to update toolbar item after sheet closed. Tested with Xcode 12b5.
struct ContentView: View {
#State var show = false
#State var buttonTitle = "button A"
var body: some View {
NavigationView {
Text("Hello World!")
.toolbar {
ToolbarItem(placement: .principal) {
Button {
show.toggle()
} label: {
Text(buttonTitle)
}
.sheet(isPresented: $show) {
SelectTitle(buttonTitle: buttonTitle) {
self.buttonTitle = $0
}
}
}
}
}
}
}
struct SelectTitle: View {
#Environment(\.presentationMode) var presentationMode
#State private var buttonTitle: String
let callback: (String) -> ()
init(buttonTitle: String, callback: #escaping (String) -> ()) {
_buttonTitle = State(initialValue: buttonTitle)
self.callback = callback
}
var body: some View {
Button("Button B") {
buttonTitle = "Button B"
presentationMode.wrappedValue.dismiss()
}
.onDisappear {
callback(buttonTitle)
}
}
}
Move sheet(...) outside of ToolbarItem scope like this:
NavigationView {
..
}.sheet(...)

How to get the picker in SwiftUI to output the chosen value?

I have a picker in swiftUI that are used to show Units of Measure and convert between them. The picker is displayed correctly, but the values are not selected when choosing one of them.
import SwiftUI
struct ContentView: View {
#State private var originalValue = ""
#State private var originalUnit = ""
#State private var convertedUnit = ""
let lenghtUnits = ["meters", "miles", "yards"]
var convertedValue : Double {
return 0 // for now..
}
var body: some View {
NavigationView {
Form {
Section(header: Text("From:")) {
TextField("Value:", text: $originalValue)
.keyboardType(.decimalPad)
Picker("fromUnit" , selection: $originalUnit) {
ForEach(0 ..< lenghtUnits.count) {
Text("\(self.lenghtUnits[$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
}
Section(header: Text("Result")) {
Text("\(convertedValue)")
}
}
.navigationBarTitle("Convert It")
}
}
}
try this. (added tags to your text and made your selection value an int)
struct ContentView: View {
#State private var originalValue = ""
#State private var originalUnit = 0
#State private var convertedUnit = ""
let lenghtUnits = ["meters", "miles", "yards"]
var convertedValue : Double {
return 0 // for now..
}
var body: some View {
NavigationView {
Form {
Section(header: Text("From:")) {
TextField("Value:", text: $originalValue)
.keyboardType(.decimalPad)
Picker("fromUnit" , selection: $originalUnit) {
ForEach(0 ..< lenghtUnits.count) { index in
Text("\(self.lenghtUnits[index])").tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
}
Section(header: Text("Result")) {
Text("\(convertedValue)")
}
}
.navigationBarTitle("Convert It")
}
}
}

Refreshing a SwiftUI List

Ím trying to refresh this List whenever I click on a NavLink
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: ListFeedItemDetail(idx: i).environmentObject(self.feed)) {
ListFeedItem(item: self.$feed.items[i])
}
}
}
The list is made out of an array inside an environment object.
The problem: It does only refresh when I switch to another tab or close the app
I had used a modal View before and it worked there. (I did it with .onAppear)
Any Ideas?
Example
Problem: When you tap on an item in the list and tap the toggle button the EnvironmentObject is changed but this changed is only reflected when I change the tab and change it back again
import SwiftUI
import Combine
struct TestView: View {
#State var showSheet: Bool = false
#EnvironmentObject var feed: TestObject
func addObjects() {
var strings = ["one","two","three","four","five","six"]
for s in strings {
var testItem = TestItem(text: s)
self.feed.items.append(testItem)
}
}
var body: some View {
TabView {
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: detailView(feed: self._feed, i: i)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}
}
}
}
.tabItem({ Text("Test") })
.tag(0)
Text("Blank")
.tabItem({ Text("Test") })
.tag(0)
}.onAppear {
self.addObjects()
}
}
}
struct detailView: View {
#EnvironmentObject var feed: TestObject
var i: Int
var body: some View {
VStack {
Text(feed.items[i].text)
Text(feed.items[i].read.description)
Button(action: { self.feed.items[self.i].isRead.toggle() }) {
Text("Toggle read")
}
}
}
}
final class TestItem: ObservableObject {
init(text: String) {
self.text = text
self.isRead = false
}
static func == (lhs: TestItem, rhs: TestItem) -> Bool {
lhs.text < rhs.text
}
var text: String
var isRead: Bool
let willChange = PassthroughSubject<TestItem, Never>()
var read: Bool {
set {
self.isRead = newValue
}
get {
self.isRead
}
}
}
class TestObject: ObservableObject {
var willChange = PassthroughSubject<TestObject, Never>()
#Published var items: [TestItem] = [] {
didSet {
willChange.send(self)
}
}
}
I had a similar problem, this is the hack I came up with.
In your "TestView" declare:
#State var needRefresh: Bool = false
Pass this to your "detailView" destination, such as:
NavigationLink(destination: detailView(feed: self._feed, i: i, needRefresh: self.$needRefresh)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}.accentColor(self.needRefresh ? .white : .black)
}
Note ".accentColor(self.needRefresh ? .white : .black)" to force a refresh when "needRefresh"
is changed.
In your "detailView" destination add:
#Binding var needRefresh: Bool
Then in your "detailView" in your Button action, add:
self.needRefresh.toggle()