Passing data from enum to sheet triggered inside ForEach loop - ios

I've got an ForEach loop inside my VStack, so that for every element in my enum a new "cell" is created. This works just fine. I can pass the title and the number for each cell, but in each cell there is a button which is toggling a sheet view. Each sheet should contain the according text in a scroll view. The text therefore is also given in the enum.
Problem: But when I'm trying to pass that infoText via the element.infoText for every sheet the infoText of the first element in the enum gets presented.
The ForEach loop:
struct ListView: View{
#State var infoSheetIsPresented: Bool = false
var body: some View{
VStack {
ForEach(WelcomeCardViewContent.allCases, id: \.self) {
element in
HStack {
Text(element.text)
Button(action: {
self.infoSheetIsPresented.toggle()
}) {
Image(systemName: "info.circle")
}
.sheet(isPresented: self.$infoSheetIsPresented) {
Text(element.infoText)
}
}
}
}
}
}
And here is my enum. Of course there's the InfoSheetView as well, but like i said its basically just a scroll view with text. The text gets passed with a simple "text" constant. For simplicity I've replaced the separate sheet view with a simple text view -> same problem.
enum WelcomeCardViewContent: String, CaseIterable{
case personalData
case bodyParameters
var text: String {
switch self{
case .personalData:
return "Personal Data"
case .bodyParameters:
return "Body Parameters"
}
}
var infoText: String {
switch self{
case .personalData:
return "1 Lorem ipsum dolor.."
case .bodyParameters:
return "2 Lorem ipsum dolor sit amet."
}
}
}
Thanks for your advice ^^.

Since you were losing track of the current card, I fixed this by saving the card which is going to be shown. Now the text is displayed correctly.
Here is the fixed version:
struct ListView: View {
#State private var infoSheetIsPresented: Bool = false
#State private var showingCard: WelcomeCardViewContent = .personalData
var body: some View {
ForEach(WelcomeCardViewContent.allCases, id: \.self) { element in
HStack {
Text(element.text)
Button(action: {
self.showingCard = element
self.infoSheetIsPresented.toggle()
}) {
Image(systemName: "info.circle")
}
.sheet(isPresented: self.$infoSheetIsPresented) {
Text(self.showingCard.infoText)
}
}
}
}
}

Related

Segmented picker not letting me pick a different selection

I have a segmented picker with three options that defaults to the first option. The problem is that when I select any of the other ones, it will not change for some reason.
This is what it looks like.
#Binding var transaction: TransactionModel
var body: some View {
VStack {
DatePicker(
"Transaction Date",
selection: $transaction.date,
displayedComponents: [.date]
)
Divider()
Picker("Type", selection: $transaction.type) {
ForEach(TransactionModel.TransactionType.allCases, id:\.self) { tType in
Text(tType.rawValue.capitalized)
}
}.pickerStyle(SegmentedPickerStyle())
}
.padding()
}
This is the code for the view so far, and below is the code for TransactionModel.
struct TransactionModel {
var date = Date()
var type = TransactionType.income
static let `default` = TransactionModel()
enum TransactionType: String, CaseIterable, Identifiable {
case income = "Income"
case expense = "Expense"
case transfer = "Transfer"
var id: String {self.rawValue}
}
}
One thing to note, it will work with the inline picker style. That will let me change between those three options, however, I want to use the segmented one. Can anyone help me figure out what's going on with this?
Edit: I actually found that the inline picker style doesn't seem to be working either. I added some code to display a text view based on what was selected, and it never changed from what it said when Income was selected. But that could be due to my code itself. Below is the code for that.
Picker("Type", selection: $transaction.type) {
ForEach(TransactionModel.TransactionType.allCases, id:\.self) { tType in
Text(tType.rawValue.capitalized)
}
}.pickerStyle(InlinePickerStyle())
Divider()
if(transaction.type == TransactionModel.TransactionType.income) {
Text("Income Selected")
Divider()
}else if(transaction.type == TransactionModel.TransactionType.expense) {
Text("Expense Selected")
Divider()
}

Setting a shared title within a common Header View amongst Views; per Active View

Goal: To use a common header View containing a shared title Text().
Scenario: I have multiple Views that share a common tab space within the one container tab View that contains a struct Header that is to be shared.
👉 This is a (many : 1) scenario.
Note: I don't want to use a NavigationView because it screws up landscape mode. A simple small header View is fine. I just need to populate the shared Title space amongst the member Views.
I don't want to merely add duplicate headers (having exactly the same layout) for each member View.
Several ideas: I need the header to respond to the 'change of title' event so I can see the new title.
So I believe I could use 1) #Binder(each member View) --> #State (shared Header View) or 2) #Environment.
I don't know how I could fit #1 into this particular scenario.
So I'm playing with #2: Environment Object.
DesignPattern: Main Header View's title set by multiple Views so the Header View is not aware of the multiple Views:
I'm not getting the EnvironmentObject paradigm to work.
Here's the codes...
MainView:
import SwiftUI
// Need to finish this.
class NYTEnvironment {
var title = "Title"
var msg = "Mother had a feeling..."
}
class NYTSettings: ObservableObject {
#Published var environment: NYTEnvironment
init() {
self.environment = NYTEnvironment()
}
}
struct NYTView: View {
var nytSettings = NYTSettings()
#State var selectionDataSegmentIndex = 0
var bindingDataSourceSegment: Binding<Int> {
.init(get: {
selectionDataSegmentIndex
}, set: {
selectionDataSegmentIndex = $0
})
}
var body: some View {
let county = 0; let state = 1; let states = 2
VStack {
NYTHeaderView()
SegmentAndDataPickerVStack(spacing: 10) {
if let segments = Source.NYT.dataSegments {
Picker("NYT Picker", selection: bindingDataSourceSegment) {
ForEach(segments.indices, id: \.self) { (index: Int) in
Text(segments[index])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
if selectionDataSegmentIndex == county {
NYTCountyView()
} else if selectionDataSegmentIndex == state {
NYTStateView()
} else if selectionDataSegmentIndex == states {
NYTStatesView()
}
Spacer()
}.environmentObject(nytSettings)
}
struct TrailingItem: View {
var body: some View {
Button(action: {
print("Info")
}, label: {
Image(systemName: "info.circle")
})
}
}
}
// ====================================================================================
struct NYTHeaderView: View {
#EnvironmentObject var nytSettings: NYTSettings
var body: some View {
ZStack {
Color.yellow
Text(nytSettings.environment.title)
}.frame(height: Header.navigationBarHeight)
}
}
Revision: I've added EnvironmentObject modifiers to the memberViews():
if selectionDataSegmentIndex == county {
NYTCountyView().environmentObject(NYTSettings())
} else if selectionDataSegmentIndex == state {
NYTStateView().environmentObject(NYTSettings())
} else if selectionDataSegmentIndex == states {
NYTStatesView().environmentObject(NYTSettings())
}
...
One of the member Views that's within the Main Container/Tab View (per above):
struct NYTCountyView: View {
#ObservedObject var dataSource = NYTCountyModel()
#EnvironmentObject var nytSettings: NYTSettings
...
...
}.onAppear {
nytSettings.environment.title = "Selected Counties"
if dataSource.revisedCountyElementListAndDuration == nil {
dataSource.getData()
}
}
Spacer()
...
}
Here's the compile-time error:
Modus Operandi: Set the title w/in header per member View upon .onAppear().
Problem: I'm not getting any title; just the default "Title" value.
Question: Am I on the right track? If so, what am I missing?
or... is there an alternative?
The whole problem boils down to a 'Many : 1' paradigm.
I got this revelation via taking a break and going for a walk.
So this is the proverbial 'round peg in a square hole' scenario.
What I needed was a lightly coupled relationship where the origin of the title value isn't required. Hence the use of the Notification paradigm.
The header view's title is the receiver and hence I used the .onReceive modifier:
struct NYTHeaderView: View {
#State private var title: String = ""
var body: some View {
ZStack {
Color.yellow
Text(title).onReceive(NotificationCenter.default.publisher(for: .headerTitle)) {note in
title = note.object as? String ?? "New York Times"
}
}.frame(height: Header.navigationBarHeight)
}
}
This sounds like what SwiftUI preferences was built to solve. The preferences are values collected and reduced from children for some distant ancestor to use. One notable example of this is how NavigationView gets its title - the title is set on the child, not on the NavigationView itself:
NavigationView {
Text("I am a simple view")
.navigationTitle("Title")
}
So, in your case you have some kind of title (simplified to String for brevity) that each child view might want to set. So you'd define a TitlePreferenceKey like so:
struct TitlePreferenceKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
Here, the reduce function is simply applying the last value it sees from descendants, but since you'd only ever have one child view selected it should work.
Then, to use it, you'd have something like this:
struct NYTView: View {
#State var title = ""
#State var selection = 0
var body: some View {
VStack {
Text(title)
Picker("", selection: $selection) {
Text("SegmentA").tag(0)
Text("SegmentB").tag(1)
}
switch selection {
case 0: NYTCountyView()
case 1: NYTStateView()
.preference(key: TitlePreferenceKey.self, value: "State view")
default: EmptyView()
}
}
.onPreferenceChange(TitlePreferenceKey.self) {
self.title = $0
}
}
struct NYTCountyView: View {
#State var selectedCounty = "..."
var body: some View {
VStack {
//...
}
.preference(key: TitlePreferenceKey.self, value: selectedCounty)
}
}
So, a preference can be set by the parent of, as in the example of NYTStateView, or by the child with the value being dynamic, as in the example of NYTCountyView

SwiftUI - Prevent Sections from flying/zooming to the right in List when dynamically filtering them

I originally asked this question:
SwiftUI - Dynamic List filtering animation flies to right side when data source is empty
There, I had a List without sections. I was filtering them so that it only showed the rows that contained the text inside a TextField. The solution was to wrap everything inside the List in a Section.
Unfortunately, I now need to filter Sections. Here's my code:
struct Group: Identifiable {
let id = UUID() /// required for the List
var groupName = ""
var people = [Person]()
}
struct Person: Identifiable {
let id = UUID() /// required for the List
var name = ""
}
struct ContentView: View {
#State var searchText = ""
var groups = [
Group(groupName: "A People", people: [
Person(name: "Alex"),
Person(name: "Ally"),
Person(name: "Allie")
]),
Group(groupName: "B People", people: [
Person(name: "Bob")
]),
Group(groupName: "T People", people: [
Person(name: "Tim"),
Person(name: "Timothy")
])
]
var body: some View {
VStack {
TextField("Search here", text: $searchText) /// text field
.padding()
List {
ForEach(
/// Filter the groups for those that contain searchText
groups.filter { group in
searchText.isEmpty || group.groupName.localizedStandardContains(searchText)
}
) { group in
Section(header: Text(group.groupName)) {
ForEach(group.people) { person in
Text(person.name)
}
}
}
}
.animation(.default) /// apply the animation
}
}
}
Result:
I pass in a filtered array in the ForEach to determine the Sections. However, whenever that array changes, the List animates really weirdly. The Sections zoom/fly to the right side, and come back from the left when the array includes them again. How can I avoid this animation?
If I remove .animation(.default), it doesn't animate at all, as expected. But, I would still like an animation. Is there a way to fade the changes, or slide them instead?
The solution is not using List. As long as you're not using selection and row deleting a ScrollView is basically the same.
If you want to style it a bit like the List that's also not that hard:
struct SearchAnimationExample: View {
...
var body: some View {
VStack {
TextField("Search here", text: $searchText) /// text field
.padding()
ScrollView {
VStack(spacing: 0) {
ForEach(
groups.filter { group in
searchText.isEmpty || group.groupName.localizedStandardContains(searchText)
}
) { group in
Section(header: header(title: group.groupName)) {
ForEach(group.people) { person in
row(for: person)
Divider()
}
}
}.transition(.opacity) // Set which transition you would like
// Always full width
HStack { Spacer() }
}
}
.animation(.default)
}
}
func header(title: String) -> some View {
HStack {
Text(title).font(.headline)
Spacer()
}
.padding(.horizontal)
.background(Color.gray.opacity(0.4))
}
func row(for person: Person) -> some View {
HStack {
Text(person.name)
Spacer()
}.padding()
}
}
Looks practically the same as the default list:

SwiftUI List selection has no value

I want to basically make didSelectRow like UITableView in SwiftUI.
This is the code:
struct ContentView: View {
var testData = [Foo(name: "1"),
Foo(name: "2"),
Foo(name: "3"),
Foo(name: "4"),
Foo(name: "5")]
#State var selected: Foo?
var body: some View {
NavigationView {
VStack {
List(testData, id: \.name, selection: $selected) { foo in
HStack {
Text(foo.name)
}
}.navigationBarTitle("Selected \(selected?.name ?? "")")
Button("Check:") {
print(selected?.name)
}
}
}
}
I was thought if I click the cell then selected should contains the selected value, but it's not. The selected has no value. And the cell not clickable.
So I added a Button.
NavigationView {
VStack {
List(testData, id: \.name, selection: $selected) { foo in
HStack {
Text(foo.name)
Button("Test") {
print("\(foo) is selected.")
print(selected?.name)
}
}
}.navigationBarTitle("Selected \(selected?.name ?? "")")
Button("Check:") {
print(selected?.name)
}
}
Now, click works, but actually foo is the one I want there's no need selected why selection of the List is here.
Not sure anything I missed. Should the Button is necessary for the List "didSelectRow"? thanks!
EDIT
After a bit more investigation, my current conclusion is:
For single selections, no need call List(.. selection:). But you have to use Button or OnTapGesture for clickable.
List(.. selection:) is only for edit mode, which is multiple selection, as you can see the selection: needs a set. My example should be
#State var selected: Set<Foo>?
On iOS selection works in Edit mode by design
/// Creates a list with the given content that supports selecting multiple
/// rows.
///
>> /// On iOS and tvOS, you must explicitly put the list into edit mode for
>> /// the selection to apply.
///
/// - Parameters:
/// - selection: A binding to a set that identifies selected rows.
/// - content: The content of the list.
#available(watchOS, unavailable)
public init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: () -> Content)
so you need either add EditButton somewhere, or activate edit mode programmatically, like
List(selection: $selection) {
// ... other code
}
.environment(\.editMode, .constant(.active)) // eg. persistent edit mode
Update: Here is some demo of default SwiftUI List selection
struct DemoView: View {
#State private var selection: Set<Int>?
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
List(selection: $selection) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
}.onDelete(perform: {_ in})
}
.environment(\.editMode, .constant(.active))
}
}

SwiftUI static List weird reuse behavior

I'm facing a strange behavior using a static List in SwiftUI. I can't determine if it's a SwiftUI bug or something I'm doing wrong. I have a very simple List that looks like this :
var body: some View {
List {
SettingsPickerView<TrigonometryUnit>(title: "Trigonometry Units", selection: $viewModel.trigonometryUnitIndex, items: TrigonometryUnit.allCases)
SettingsPickerView<DecimalSeparator>(title: "Decimal Separator", selection: $viewModel.decimalSeparatorIndex, items: DecimalSeparator.allCases)
SettingsPickerView<GroupingSeparator>(title: "Grouping Separator", selection: $viewModel.groupingSeparatorIndex, items: GroupingSeparator.allCases)
SettingsPickerView<ExponentSymbol>(title: "Exponent Symbol", selection: $viewModel.exponentSymbolIndex, items: ExponentSymbol.allCases)
}
}
Each cell of the List looks like this :
struct SettingsPickerView<T: Segmentable>: View {
let title: String
#Binding var selection: Int
let items: [T]
var body: some View {
Section(header: Text(title)) {
ForEach(items.indices) { index in
self.cell(for: self.items[index], index: index)
}
}
}
private func cell(for item: T, index: Int) -> some View {
print(title, item.title, items.map({ $0.title }))
return Button(action: {
self.selection = index
}, label: {
HStack {
Text(item.title)
Spacer()
if index == self.selection {
Image(systemName: "checkmark")
.font(.headline)
.foregroundColor(.rpnCalculatorOrange)
}
}
})
}
}
And finally, this is what a Segmentable object looks like:
enum GroupingSeparator: Int, CaseIterable {
case defaultSeparator
case space
case comma
}
extension GroupingSeparator: Segmentable {
var id: String {
switch self {
case .defaultSeparator:
return "groupingSeparator.default"
case .space:
return "groupingSeparator.space"
case .comma:
return "groupingSeparator.comma"
}
}
var title: String {
switch self {
case .defaultSeparator:
return "Default"
case .space:
return "Space"
case .comma:
return "Comma"
}
}
}
When the SettingsView is loaded. everything looks fine. But as soon as I start scrolling, and some other cells are instantiated, there are some cell displayed, but not the proper ones. Here is some screenshots and logs.
When the view is loaded, no scrolling, here is what the screen looks like:
But, what I got on the console is pretty weird and doesn't follow the order of the SettingsPickerView written in the main View:
Trigonometry Units Radians ["Radians", "Degrees"] <-- Fine
Trigonometry Units Degrees ["Radians", "Degrees"] <-- Fine
Decimal Separator Default ["Default", "Dot", "Comma"] <-- Fine
Decimal Separator Default ["Default", "Dot", "Comma"] <-- Fine
Trigonometry Units Degrees ["Radians", "Degrees"] <-- Not expected. Should be Grouping Separator
Trigonometry Units Radians ["Radians", "Degrees"] <-- Not expected. Should be Grouping Separator
The second section is ok and properly displayed:
But the third section is completely broken:
The third section displays its title properly, but display some of the data of the first section. I tried to add an identifier to the button in the cell because the issue looks like SwiftUI can't identify the proper data. But adding an identifier to the button broke the binding, and the checkbox don't change anymore.
private func cell(for item: T, index: Int) -> some View {
print(title, item.title, items.map({ $0.title }))
return Button(action: {
self.selection = index
}, label: {
HStack {
Text(item.title)
Spacer()
if index == self.selection {
Image(systemName: "checkmark")
.font(.headline)
.foregroundColor(.rpnCalculatorOrange)
}
}
})
.id(UUID().uuidString) // This solve the display issue but broke the binding.
}
Does someone experienced something like this before ?
Thanks in advance for your help.
Here is fixed block of code (due to used indexes only List is confused and reuses rows, so solution is to make rows identifiable by items).
Tested with Xcode 11.4
struct PickerView<T: Segmentable>: View {
// ... other code here
var body: some View {
Section(header: Text(title)) {
// Corrected section construction !!
ForEach(Array(items.enumerated()), id: \.element.id) { index, _ in
self.cell(for: self.items[index], index: index)
}
}
}
// ... other code here

Resources