Implementing Swift UI Picker - swiftui

What is the correct way to implement a Picker component with specific logic within a Section element?
I would like to have each type displayed in a separate row.
var types = ["Books", "Films", "Music"]
var body: some View {
Form {
Section(header: Text("Type")) {
TextField("Type", text: $newCategoryType)
// Picker
}
}
}

First you must have a #State property that can be updated based on what selection the user makes, say in this case we have this
#State private var selectedType = "Books"
Then you will implement a Picker SwiftUI struct as follows
Picker("Please choose a type", selection: $selectedType) {
ForEach(types, id: \.self) {
Text($0)
}
}
Note that the \.self is really important for ForEach to distinguish between each element inside the list, without which the Picker won't perform the selection action correctly.
The above is enough for doing the job of displaying each option as a row since that is the default behaviour of ForEach
Additionally if you want to customise the look and feel of the picker
you would like to see .pickerStyle() view modifier, for which the docs and examples are mentioned
Also
Tip: Because pickers in forms have this navigation behavior, it’s important you present them in a NavigationView on iOS otherwise you’ll find that tapping them doesn’t work. This might be one you create directly around the form, or you could present the form from another view that itself was wrapped in a NavigationView.

Related

Controlling the look of Labels in SwiftUI Pickers

I'm curious, is there a way to control how Label views are expressed in SwiftUI Pickers? Of course, one of the features of SwiftUI is that controls express themselves in different ways in different contexts (eg form vs not-form, iOS vs macOS, in toolbar vs out of toolbar).
Normally this feature is desirable, but in my situation, I find the disconnect of how the same label looks in the following three contexts to be a bit jarring:
When displaying the current selection of a Picker.
When displaying the choices available in the Picker menu.
When simply displaying information in a form.
In particular, 1 vs 2 is jarring. It doesn't seem right that the user chooses something from picker menu, where the SFSymbol is on the right, and the selection is shown with the SFSymbol on the left. And the way the label looks in context 1, the picker selection, is unattractive to me. The symbol and text are uncomfortably close.
Is there a way to override the default expressions so these differences are less jarring in this context? I actually would like it if all Labels could be expressed as something close to situation 3 (but with a narrower gap between SFSymbol and Text).
Here is some code to demonstrate this problem:
struct LabelInPickerLayout: View {
#State private var selection = 2
var body: some View {
Form {
Picker("Choice", selection: $selection) {
Label("First Item", systemImage: "1.circle").tag(1)
Label("Second Item", systemImage: "2.circle").tag(2)
}
.pickerStyle(.menu)
.labelsHidden()
Section("almost desired look") {
Label("First Item", systemImage: "1.circle")
}
}
}
}

Swiftui: How to Close one Tab in a TabView?

I'm learning SwiftUI and having trouble with closing Each Tab View Element.
My App shows photos from user's album by TabView with pageViewStyle one by one.
And What I want to make is user can click save button in each view, and when button is clicked, save that photo and close only that view while other photos are still displayed. So unless all photos are saved or discarded, if user clicks save button, TabView should automatically move to another one.
However, I don't know how to close only one Tab Element. I've tried to use dismiss() and dynamically changing vm.images element. Latter one actually works, but it displays awkward movement and it also requires quite messy code. How could I solve this issue?
Here is my code.
TabView {
ForEach(vm.images, id: \.self) { image in
TestView(image: image)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
struct TestView: View {
#ObservedObject var vm: TestviewModel
...
var body: some View {
VStack(spacing: 10) {
Image(...)
Spacer()
Button {
...
} label: {
Text("Save")
}
}
You need actually to remove saved image from the viewModel container, and UI will be updated automatically
literally
Button {
vm.images.removeAll { $0.id == image.id } // << here !!
} label: {
Text("Save")
}
You need to use the selection initializer of TabView in order to control what it displays. So replace TabView with:
TabView(selection: $selection)
Than add a new property: #State var selection: YourIdType = someDefaultValue, and in the Button action you set selection to whatever you want to display.
Also add .tag(TheIdTheViewWillUse) remember that whatever Id you use must be the same as your selection variable. I recommend you use Int for the simple use.

Getting two different results using ForEach in SwiftUI

I have list of structs that I display in the view using a ForEach, I am trying to add to the list and when the user taps they see information for that item. Using two approaches I get two different results.
First Approach
The view updates the view when items are added perfectly, but changing the note in the Details changes values of all notes.
#Binding var person: Person
ForEach(self.person.notes) {
note in
DetailsCard(person: self.$person, note: notes)
}
Second Approach
The view does not update when notes are added, only when the view reappears does it show the new items. But I when the items are shown, the details view works as expected.
#Binding var person: Person
ForEach(self.person.notes.indices) { index in
VStack {
DetailsCard(person: self.$person, note: self.person.notes[index])
}
}
DetailView
#Binding var person: Person
#State var note: Note
This should be a fairly simple task but working with SwiftUI I am confused by the two different results for similar approaches. Does anyone have any ideas on why this is occurring and how to correctly dynamically update the view and pass the values.
Try using dynamic ForEach loops, ie. with an explicit id parameter:
ForEach(self.person.notes, id:\.self)
ForEach(self.person.notes.indices, id:\.self)
Note: id must conform to Hashable. If you decide to use self as an id, it will only work if your item conforms to Hashable. Otherwise you may want to use some property (or just add the conformance).

SwiftUI - Issue with ContextMenu on a page containing a List view overriding the ContextMenu of another page (watchOS)

I have reduced my problem to the following views which are embedded in two WKHostingControllers paging between each other.
struct FirstContentView: View {
var body: some View {
Text("FirstContentView")
.contextMenu {
Text("FirstContextMenu")
}
}
}
struct SecondContentView: View {
var body: some View {
List {
Text("SecondContentView")
}.contextMenu {
Text("SecondContextMenu")
}
}
}
When I run it this happens. As soon as page 2 is loaded the context menu of page 1 remains replaced by the one of page 2. This issue only happens if a List view is anywhere in the body of SecondContentView, even if the .contextMenu is not attached to it or the List is encapsulated into another View. Replacing List for example with Group works, but I need the list behaviour for my items.
Has anybody also run into this issue? How can I fix it so the context menu works as intended (?) on both pages?

SwiftUI how to detect when a view has the focus?

I have two List's in a view and want to be able to determine which list currently has the focus in order to show the correct details of the selected item in the list in a details panel.
The following code never seems to get called, can anyone indicate whether there is another correct way to determine when focus changes.
struct StoreList: View {
#EnvironmentObject private var database: Database
#Binding var selectedStore: Store?
var body: some View {
List(selection: $selectedStore) {
ForEach(database.stores, id: \.self) { store in
StoreRow(store: store).tag(store)
.focusable(true, onFocusChange: { isFocused in
print("focus changed")
if isFocused {
self.database.selectedType = .store
}
})
}
}
.focusable(true, onFocusChange: { isFocused in
print("focus changed")
if isFocused {
self.database.selectedType = .store
}
})
}
}
In the meantime I will explore detecting mouse clicks on the Rows since the user would need to click on an item in the list to move the focus.
Currently I am setting the selectedType value when an item changes (i.e. $selectedStore) in the view model (database) but if the user selected the already selected item in the other list then the value does not get updated but the List and list item does get the focus - well the visual colour change indicates it has the focus.
EDIT:
I have also tried processing the onTapGesture callback which works fine except it replaces the List rows default behaviour. How can I make sure the event is passed through to the List as this might work then.
The easiest method of reacting to focus is
struct DummyView: View {
#Environment(\.isFocused) var isFocused
var body: some View {
Text("My view")
.padding()
.background(isFocused ? Color.orange : Color.black)
}
}