I made some code which is representing View and two Sections in it:
#State var firstCases: FirstTestCases = .first
#State var secondCases: SecondTestCases = .newFirst
var body: some View {
Section(
header: Text("First header"),
footer: Text("First footer"),
content: {
Picker("", selection: $firstCases) {
Text("First case").tag(FirstTestCases.first)
Text("Second case").tag(FirstTestCases.second)
Text("Third case").tag(FirstTestCases.third)
}
.onChange(of: firstCases) { _ in print("Do something") }
.pickerStyle(.segmented)
.padding(.vertical, 4)
}
)
Section(
header: Text("Second header"),
footer: Text("Second footer"),
content: {
Picker("", selection: $secondCases) {
Text("New first case").tag(SecondTestCases.newFirst)
Text("New second case").tag(SecondTestCases.newSecond)
Text("New third case").tag(SecondTestCases.newThird)
}
.onChange(of: secondCases) { _ in print("Do something") }
.pickerStyle(.segmented)
.padding(.vertical, 4)
if secondCases == .newThird {
Button("First button") {
print("Do something")
}
}
Button("Second button", action: {
print("Do something")
})
}
)
}
enum FirstTestCases: Equatable {
case first
case second
case third
}
enum SecondTestCases: Equatable {
case newFirst
case newSecond
case newThird
}
I want to implement option of sorting these sections (I made only 2 here, I will have 7+ sections and section can be different (one section can have few buttons and some sections dont have buttons)). To have option of sorting, I need to have some data in array. So I wanted to create array of Sections.
#State var data: [Section] = []
I was getting error Generic parameter 'Content' could not be inferred., so I made it like this
#State var data: [Section<AnyView, AnyView, AnyView>] = []
I created function, which will append data to array. (Appending already created sections, so later I can use ForEach and use modifier .move)
private func prepareData() {
data
.append(
contentsOf: Section(
header: Text("Second header"),
footer: Text("Second footer"),
content: {
Picker("", selection: $secondCases) {
Text("New first case").tag(SecondTestCases.newFirst)
Text("New second case").tag(SecondTestCases.newSecond)
Text("New third case").tag(SecondTestCases.newThird)
}
.onChange(of: secondCases) { _ in print("Do something") }
.pickerStyle(.segmented)
.padding(.vertical, 4)
if secondCases == .newThird {
Button("First button") {
print("Do something")
}
}
Button("Second button", action: {
print("Do something")
})
}
)
)
}
But problem I am getting is -
Instance method 'append(contentsOf:)' requires that 'Section<Text, TupleView<(some View, Button?, Button)>, Text>' conform to 'Sequence'
Why is this problem occuring? Is this good approach of implementing Section sorting, or is there some better?
Related
I am trying to implement the behavior in a TabView when the user taps the same tab multiple times, such as in the iOS AppStore app. First tap: switch to that view, second tap: pop to root, third tap: scroll to the top if needed.
The code below works fine for switching and didTap() is called for every tap.
import SwiftUI
enum Tab: String {
case one
case two
}
struct AppView: View {
#State private var activeTab = Tab.one
var body: some View {
TabView(selection: $activeTab.onChange(didTap)) {
One()
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
Two()
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
func didTap(to value: Tab) {
print(value) // this captures every tap
}
}
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler(newValue)
}
)
}
}
What I am struggling with, is how to tell either One or Two that it was tapped for a second or third time? (How to pop and scroll is not the issue).
I have seen this: TabView, tabItem: running code on selection or adding an onTapGesture but it doesn't explain how to run code in one of the views.
Any suggestions?
You can record additional taps (of same value) in an array. The array count gives you the number of taps on the same Tab.
EDIT: now with explicit subview struct.
struct ContentView: View {
#State private var activeTab = Tab.one
#State private var tapState: [Tab] = [Tab.one] // because .one is default
var body: some View {
TabView(selection: $activeTab.onChange(didTap)) {
SubView(title: "One", tapCount: tapState.count)
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
SubView(title: "Two", tapCount: tapState.count)
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
func didTap(to value: Tab) {
print(value) // this captures every tap
if tapState.last == value {
tapState.append(value) // apped next tap if same value
print("tapped \(tapState.count) times")
} else {
tapState = [value] // reset tap state to new tab selection
}
}
}
struct SubView: View {
let title: String
let tapCount: Int
var body: some View {
VStack {
Text("Subview \(title)").font(.title)
Text("tapped \(tapCount) times")
}
}
}
Although the answer by #ChrisR did answer my question, I couldn't figure out the next step, i.e. the logic when to pop-to-root or scroll-to-the-top based on the number of taps for a SubView. After lots of reading and trial and error, I recently came across this article: https://notificare.com/blog/2022/11/25/a-better-tabview-in-swiftui/
Inspired by this article, but with some modifications, I came up with the following which does exactly what I was looking for.
The two main changes are:
An EmptyView with an id is added as the first (but invisible) row in the List to be used as an anchor by proxy.scrollTo().
Instead of the global #StateObject var appState that stores the navigation paths for the subviews, I added the paths as separate #State properties. This avoids the Update NavigationAuthority bound path tried to update multiple times per frame. warning.
Hopefully this is helpful for someone.
enum Tab: String {
case one
case two
}
struct ContentView: View {
#State var selectedTab = Tab.one
#State var oneNavigationPath = NavigationPath()
#State var twoNavigationPath = NavigationPath()
var body: some View {
ScrollViewReader { proxy in
TabView(selection: tabViewSelectionBinding(proxy: proxy)) {
SubView(title: "One", path: $oneNavigationPath)
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
SubView(title: "Two", path: $twoNavigationPath)
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
}
private func tabViewSelectionBinding(proxy: ScrollViewProxy) -> Binding<Tab> {
Binding<Tab>(
get: { selectedTab },
set: { newValue in
if selectedTab == newValue {
switch selectedTab {
case .one:
if oneNavigationPath.isEmpty {
withAnimation {
proxy.scrollTo(Tab.one, anchor: .bottom)
}
} else {
withAnimation {
oneNavigationPath = NavigationPath()
}
}
case .two:
if twoNavigationPath.isEmpty {
withAnimation {
proxy.scrollTo(Tab.two, anchor: .bottom)
}
} else {
withAnimation {
twoNavigationPath = NavigationPath()
}
}
}
}
selectedTab = newValue
}
)
}
}
struct SubView: View {
let title: String
let items = Array(1 ... 100)
#Binding var path: NavigationPath
var body: some View {
NavigationStack(path: $path) {
List {
EmptyView()
.id(Tab(rawValue: title.lowercased()))
ForEach(items, id: \.self) { item in
NavigationLink(value: item) {
Text("Item \(item)")
}
}
}
.navigationTitle(title)
.navigationDestination(for: Int.self) { item in
Text("Item \(item)")
}
}
}
}
I have List within a TabView and allowing the user to scroll to the top when they double tap a tab. I'm using a ScrollViewReader to scroll to a specific anchor. However, it is not fully scrolling to the top of the list because of the navigation title, see the title overlapping the content:
I'm using the technique from this blog post for more context. Below is a working sample:
struct ContentView: View {
#State private var _selectedTab: SelectedTab = .one
#State private var tabbedTwice = false
var selectedTab: Binding<SelectedTab> {
Binding(
get: { _selectedTab },
set: {
if $0 == _selectedTab {
tabbedTwice = true
}
_selectedTab = $0
}
)
}
enum SelectedTab: String {
case one
case two
}
var body: some View {
ScrollViewReader { proxy in
TabView(selection: selectedTab) {
NavigationView {
List {
Section {
ForEach(1...50, id: \.self) { index in
Text("Item \(index.formatted())")
}
}
.id(SelectedTab.one.rawValue)
Section {
Text("Section 2")
}
}
.navigationTitle("First")
}
.tabItem {
Label("One", systemImage: "clock.arrow.circlepath")
}
.tag(SelectedTab.one)
NavigationView {
List {
Section(header: Text("Header").id(SelectedTab.two.rawValue)) {
ForEach(50...100, id: \.self) { index in
Text("Item \(index.formatted())")
}
}
Section {
Text("Section 2")
}
}
.navigationTitle("Second")
}
.tabItem {
Label("Two", systemImage: "list.bullet")
}
.tag(SelectedTab.two)
}
.onChange(of: tabbedTwice) {
guard $0 else { return }
withAnimation { proxy.scrollTo(_selectedTab.rawValue, anchor: .top) }
tabbedTwice = false
}
}
}
}
Is there a better place to put the anchor identifier? I tried putting on the first section and also the section header which worked better but still not scrolling to the very top. How can this be achieved?
I have this AddWorkoutView and I am trying to build some forms similar to what Apple did with "Add new contact" sheet form.
Right now I am trying to add a form more complex than a simple TextField (something similar to "add address" from Apple contacts but I am facing the following issues:
in the Exercises section when pressing on a new created entry (exercise), both Picker and delete Button are triggered at the same time and the Picker gets automatically closed as soon as it gets open and the selected entry is also deleted when going back to AddWorkoutView.
Does anyone have any idea on how Apple implemented this kind of complex form like in the screenshow below?
Thanks to RogerTheShrubber response here I managed to somehow implement at least the add button and to dynamically display all the content I previously added, but I don't know to bring together multiple TextFields/Pickers/any other stuff in the same form.
struct AddWorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#EnvironmentObject var dateModel: DateModel
#Environment(\.presentationMode) var presentationMode
#State var workout: Workout = Workout()
#State var exercises: [Exercise] = [Exercise]()
func getBinding(forIndex index: Int) -> Binding<Exercise> {
return Binding<Exercise>(get: { workout.exercises[index] },
set: { workout.exercises[index] = $0 })
}
var body: some View {
NavigationView {
Form {
Section("Workout") {
TextField("Title", text: $workout.title)
TextField("Description", text: $workout.description)
}
Section("Exercises") {
ForEach(0..<workout.exercises.count, id: \.self) { index in
HStack {
Button(action: { workout.exercises.remove(at: index) }) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.padding(.horizontal)
}
Divider()
VStack {
TextField("Title", text: $workout.exercises[index].title)
Divider()
Picker(selection: getBinding(forIndex: index).type, label: Text("Type")) {
ForEach(ExerciseType.allCases, id: \.self) { value in
Text(value.rawValue)
.tag(value)
}
}
}
}
}
Button {
workout.exercises.append(Exercise())
} label: {
HStack(spacing: 0) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.green)
.padding(.trailing)
Text("add exercise")
}
}
}
}
.navigationTitle("Create new Workout")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
}
.accessibilityLabel("Cancel adding Workout")
}
ToolbarItem(placement: .confirmationAction) {
Button {
} label: {
Text("Done")
}
.accessibilityLabel("Confirm adding the new Workout")
}
}
}
}
}
After removing all the items from a list and then adding the items back to the list, each list item is indented like it is in edit mode and swipe actions are unavailable. I only see the issue when I have the conditional checking if the array is empty.
struct TestView: View {
#State var categories = ["dog", "cat"]
var body: some View {
VStack {
if(categories.isEmpty){
Button ("Add category"){
categories = ["dog", "cat"]
}
} else {
List {
ForEach(categories.indices, id: \.self) { i in
Text(categories[i])
.swipeActions(allowsFullSwipe: false) {
Button(role: .destructive) {
categories.remove(at: i)
} label: {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
}
}
}
}
Before removing items from array:
After removing items and adding new items to array:
here is an example that shows that deletion is not the problem
struct TestView: View {
#State var categories = ["dog", "cat"]
var body: some View {
VStack {
if(categories.isEmpty){
Button ("Add category"){
categories = ["dog", "cat"]
}
} else {
List {
ForEach(categories.indices, id: \.self) { i in
Text(categories[i])
// .swipeActions(allowsFullSwipe: false) {
// Button(role: .destructive) {
// categories.remove(at: i)
// } label: {
// Label("Delete", systemImage: "trash.fill")
// }
// }
.onTapGesture {
categories.remove(at: i)
}
}
}
}
}
}}
the problem is that after deleting an element from the list with swipeActions the list is supposed to reposition itself, doing so just after deleting the last element from the list with swipeActions you decide to disappear the list so it will not have the time to finish his action.
I suggest the following code which works fine
struct TestView: View {
#State var categories = ["dog", "cat"]
var body: some View {
VStack {
if(categories.isEmpty){
Button ("Add category"){
categories = ["dog", "cat"]
}
}
List {
ForEach(categories.indices, id: \.self) { i in
Text(categories[i])
.swipeActions(allowsFullSwipe: false) {
Button(role: .destructive) {
categories.remove(at: i)
} label: {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
// don't display if categories.isEmpty
.frame(height: categories.isEmpty ? 0 : nil)
}
}}
Here's a possible solution. You could try using onDelete for this, documentation is here. I also included onMove if needed and added a button which is only active when the array is empty.
struct ContentView: View {
#State private var animals = [
"Dog",
"Cat",
]
var body: some View {
NavigationView {
List {
ForEach(animals, id: \.self) { animal in
Text(animal)
}
.onDelete { self.delete(at :$0) }
.onMove { self.move(from: $0, to: $1) }
}
.navigationTitle("Animals")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
self.animals = [
"Dog",
"Cat",
]
}, label: {
Label("Add", systemImage: "plus")
.labelStyle(.iconOnly)
}).disabled(!animals.isEmpty)
}
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
}
}
func delete(at: IndexSet) {
for i in at {
animals.remove(at: i)
}
}
func move(from: IndexSet, to: Int) {
animals.move(fromOffsets: from, toOffset: to)
}
}
The requirement is to display a banner above the tabbar. So that the banner does not disappear when the tabs are changed? How can I achieve it?
I can think of starting points, to help you see ways to approach this, but I don't think either idea really meets your requirements. It will depend on the details of whether the banner is permanent, where its content comes from, etc.
The first idea:
struct BannerView : View {
var text : String
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
Text(text)
Spacer()
}.background(Color.orange)
}
}
}
Then you can include this in a ZStack along with your tabs:
TabView {
ZStack {
Text("Tab 1")
BannerView("BANNER")
}.tabItem { Text("Home") }
ZStack {
Text("Tab 2")
BannerView("BANNER")
}.tabItem { Text("History") }
}
The second idea uses the BannerView from the first idea, but in a slightly cleaner way, still not great:
struct TabWrapperWithOptionalBanner<Content> : View where Content : View {
var showBanner : Bool
var content : Content
init(showBanner : Bool, #ViewBuilder content: () -> Content) {
self.showBanner = showBanner
self.content = content()
}
var body: some View {
ZStack {
content
if showBanner {
BannerView(text: "BANNER")
}
}
}
}
then your ContentView looks like this:
TabView {
TabWrapperWithOptionalBanner(showBanner: showBanner) {
Text("Tab 1")
}.tabItem { Text("Home") }
TabWrapperWithOptionalBanner(showBanner: showBanner) {
Text("Tab 2")
}.tabItem { Text("History") }
}
Update Try a pair of TabViews bound to the same State:
struct BanneredTabView: View {
#State private var selected = panels.one
var body: some View {
VStack {
TabView(selection: $selected) {
panels.one.label.tag(panels.one)
panels.two.label.tag(panels.two)
panels.three.label.tag(panels.three)
}
.tabViewStyle(PageTabViewStyle())
Text("Banner")
.frame(height: 40, alignment: .top)
TabView(selection: $selected) {
ForEach(panels.allCases) { panel in
Text("").tabItem {
panel.label
}
.tag(panel)
}
}
.frame(height: 30)
}
}
enum panels : Int, CaseIterable, Identifiable {
case one = 1
case two = 2
case three = 3
var label : some View {
switch self {
case .one:
return Label("Tab One", systemImage: "1.circle")
case .two:
return Label("Tab Two", systemImage: "2.square")
case .three:
return Label("Tab Three", systemImage: "asterisk.circle")
}
}
// so the enum can be identified when enumerated
var id : Int { self.rawValue }
}
}