Intercept custom action on TabView - swift

I was recommended to use the standard components so, instead of building my own TabView, I used SwiftUI's TabView. I only need a custom action that doesn't activate a view but triggers an action.
I almost succeeded to do it with the following code:
import SwiftUI
struct ContentView: View {
#State var value: Int = 1
private var adapterValue: Binding<Int> {
Binding<Int>(get: {
if (self.value == 4) {
return 2
} else {
return self.value
}
}, set: {
if $0 == 4 {
self.value = 2
} else {
self.value = $0
}
})
}
var body: some View {
TabView(selection: self.adapterValue, content: {
Text("A").tabItem {
VStack {
Text("A")
}
}
.tag(1)
Text("B").tabItem {
VStack {
Text("B")
}
}
.tag(2)
Text("C").tabItem {
VStack {
Text("C")
}
}
.tag(3)
Text("D").tabItem {
VStack {
Text("D")
}
}
.tag(4)
})
}
}
It works well, except for when tab 2 (B) is activated and I press 4 (D), tab 4 (D) is activated. This should not happen, but I'm unsure what I'm missing.
I would like to understand the reason why this doesn't work as I expected and how I can prevent this behavior.

you could try this:
struct ContentView: View {
#State var value: Int = 1
var body: some View {
TabView(selection: $value) {
Text("A").tabItem {
VStack {
Text("A")
}
}
.tag(1)
Text("B").tabItem {
VStack {
Text("B")
}
}
.tag(2)
Text("C").tabItem {
VStack {
Text("C")
}
}
.tag(3)
Text("D").tabItem {
VStack {
Text("D")
}
}
.tag(4)
}
.onChange(of: value) { val in
if val == 4 {
self.value = 2
}
}
}
}

Related

SwiftUI: Call a function of a programmatically created view

I am trying to make a SwiftUI ScrollView scroll to a certain point in an abstracted view when a button is pressed in a view which is calling the abstracted view programmatically. Here is my code:
struct AbstractedView: View {
#Namespace var view2ID
var body: some View {
ScrollView {
VStack {
View1()
View2()
.id(view2ID)
View3()
}
}
}
func scrollToView2(_ proxy: ScrollViewProxy) {
proxy.scrollTo(view2ID, anchor: .topTrailing)
}
}
As you can see, when scrollToView2() is called (in a ScrollViewReader), the AbstractedView scrolls to view2ID. I am creating a number of AbstractedView's programmatically in a different View:
struct HigherView: View {
var numAbstractedViewsToMake: Int
var body: some View {
VStack {
HStack {
ForEach (0..<numAbstractedViewsToMake, id: \.self) { _ in
AbstractedView()
}
}
Text("button")
.onTapGesture {
/* call each AbstractedView.scrollToView2()
}
}
}
}
If I stored these views in an array in a struct inside my HigherView with a ScrollViewReader for each AbstractedView would that work? I feel as though there has to be a nicer way to achieve this, I just have no clue how to do it. I am new to Swift so thank you for any help.
P.S. I have heard about UIKit but I don't know anything about it, is this the right time to be using that?
Using the comments from #Asperi and #jnpdx, I was able to come up with a more powerful solution than I needed:
class ScrollToModel: ObservableObject {
enum Action {
case end
case top
}
#Published var direction: Action? = nil
}
struct HigherView: View {
#StateObject var vm = ScrollToModel()
var numAbstractedViewsToMake: Int
var body: some View {
VStack {
HStack {
Button(action: { vm.direction = .top }) { // < here
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
Button(action: { vm.direction = .end }) { // << here
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
Divider()
HStack {
ForEach(0..<numAbstractedViewsToMake, id: \.self) { _ in
ScrollToModelView(vm: vm)
}
}
}
}
}
struct AbstractedView: View {
#ObservedObject var vm: ScrollToModel
let items = (0..<200).map { $0 } // this is his demo
var body: some View {
VStack {
ScrollViewReader { sp in
ScrollView {
LazyVStack { // this bit can be changed accordingly
ForEach(items, id: \.self) { item in
VStack(alignment: .leading) {
Text("Item \(item)").id(item)
Divider()
}.frame(maxWidth: .infinity).padding(.horizontal)
}
}.onReceive(vm.$direction) { action in
guard !items.isEmpty else { return }
withAnimation {
switch action {
case .top:
sp.scrollTo(items.first!, anchor: .top)
case .end:
sp.scrollTo(items.last!, anchor: .bottom)
default:
return
}
}
}
}
}
}
}
}
Thank you both!

SwiftUI, assign view to variable

I have the following view:
struct ChordPadView: View {
[...]
init() {
[...]
}
var body: some View {
[...]
if globalState.interfaceMode == .Normal {
HStack {
[...]
SomeView(playChord, stopChord) {
VStack {
Text(showType ? "\(chord.note)\(chord.type)" : chord.note)
.font(Font.custom("AeroMaticsBold", size: 15))
if (!self.hideNumeral) {
Text(self.numeral ?? chord.numeral ?? "")
.font(Font.custom("AeroMaticsBold", size: 8))
}
}
}
}
} else {
SomeOtherView(playChord, stopChord) {
VStack {
Text(showType ? "\(chord.note)\(chord.type)" : chord.note)
.font(Font.custom("AeroMaticsBold", size: 15))
if (!self.hideNumeral) {
Text(self.numeral ?? chord.numeral ?? "")
.font(Font.custom("AeroMaticsBold", size: 8))
}
}
}
}
}
}
Basically, I have two very different view, SomeOtherView and SomeOtherView and within the logic of my view I need to pass to either one or the other the very same block of content.
How can I refactor my code in order to avoid the duplication and stay DRY? Is there a way to assign the VStack block to a variable to use it in multiple places?
Create a custom view for the VStack. Either use it directly in the if-else statement, or pass it to ChordPadView.
struct ChordPadView: View {
var body: some View {
if (true) {
HStack {
VStackView()
}
} else {
VStackView()
}
}
}
Or
struct ChordPadView<Content: View>: View {
var content: Content
init(content: Content) {
self.content = content
}
var body: some View {
if (true) {
HStack {
VStackView()
}
} else {
VStackView()
}
}
}
struct VStackView: View {
var body: some View {
VStack{
Text("VStack View")
}
}
}
The else statment is never executed since the condition is always true.

SwiftUI: Unable to access Edit Mode within TabView

I currently have a view HostView that provides HostSummaryView and EditHostSummaryView, where the former responds to editMode.wrappedValue? == .inactive. HostView looks like this:
struct HostView: View {
#Environment(\.editMode) var editMode
var body: some View {
HStack {
EditButton()
}
if editMode?.wrappedValue == .inactive {
HostSummary()
} else {
EditHostSummary()
}
}
}
I have a RootView that contains a TabView, which looks like this:
struct RootView: View {
#State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
View1()
.onTapGesture { self.selectedTab = 0 }
.tag(0)
View2()
.tag(1)
HostView()
.tag(2)
}
}
}
I tried passing the #Environment(\.editMode) var editMode to HostView, but that did not fix the problem. The EditButton does not toggle editMode in the HostView. However, HostView works when I access it through a non-TabView view.
How can I get this to work?
I couldn't find a previous question about this but it is known that some things don't work from within a TabView you have to push it down a View.
I think it is considered a bug.
struct EditableHost: View {
#State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
Text("View 1")
.onTapGesture { self.selectedTab = 0 }
.tag(0)
Text("View 2")
.tag(1)
ParentHostView()
.tabItem { Text("host") }
.tag(2)
}
}
}
struct ParentHostView: View {
#State var active: Bool = true
var body: some View {
NavigationView{
NavigationLink(
destination: HostView(),
isActive: $active,
label: {
Text("HOST")
})
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct HostView: View {
#Environment(\.editMode) var editMode
var body: some View {
VStack{
HStack {
EditButton()
}
if editMode?.wrappedValue == .inactive {
Text("HostSummary")
} else {
Text("EditHostSummary")
}
}.navigationBarBackButtonHidden(true)
}
}
I encounter the same case(Xcode 13.3, iOS 15.4). You can customize an edit button and everything should be ok.
struct HostView: View {
#Environment(\.editMode) var editMode
var body: some View {
HStack {
Button {
if editMode != nil{
if editMode!.wrappedValue == .inactive{
editMode!.wrappedValue = .active
}else{
if editMode!.wrappedValue == .active{
editMode!.wrappedValue = .inactive
}
}
}
} label: {
Text(editMode?.wrappedValue.isEditing == true ? "Done" : "Edit")
}
}
if editMode?.wrappedValue == .inactive {
HostSummary()
} else {
EditHostSummary()
}
}
}

SwiftUI How to create LazyVStack with selection as List

I would like to have something like List(selection: ) in LazyVStack.
The problem is that I don't know how to manage the content to split in each element that it contains.
What I've tried to do:
public struct LazyVStackSelectionable<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {
let content: Content
var selection: Binding<Set<SelectionValue>>?
#Environment(\.editMode) var editMode
public init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: () -> Content) {
self.content = content()
self.selection = selection
}
public var body: some View {
if self.editMode?.wrappedValue == EditMode.active {
HStack {
content //here I would like to have something like ForEach (content, id:\.self)
Button(action: {
//add the UUID to the list of selected item
}) {
Image(systemName: "checkmark.circle.fill")
//Image(systemName: selection?.wrappedValue.contains(<#T##member: Hashable##Hashable#>) ? "checkmark.circle.fill" : "circle")
}
}
}
else {
content
}
}
}
struct ListView: View {
#State private var editMode: EditMode = .inactive
#State private var selection = Set<UUID>()
#State private var allElements: [MyElement] = [MyElement(id: UUID(), text: "one"),
MyElement(id: UUID(), text: "two" ),
MyElement(id: UUID(), text: "tree" )
]
var body: some View {
NavigationView {
VStack {
Divider()
Text("LazyVStack")
.foregroundColor(.red)
LazyVStack {
ForEach(allElements, id: \.self) { element in //section data
Text(element.text)
}
}
Divider()
Text("LazyVStackSelectionable")
.foregroundColor(.red)
LazyVStackSelectionable(selection: $selection) {
ForEach(allElements, id: \.self) { element in //section data
Text(element.text)
}
}
Divider()
}
.environment(\.editMode, self.$editMode)
.navigationBarTitle(Text("LIST"), displayMode: .inline)
.navigationBarItems(//EDIT
trailing:
Group {
HStack (spacing: 15) {
self.editButton
self.delInfoButton
.contentShape(Rectangle())
}
}
)
}
}
//MARK: EDIT MODE
private func deleteItems() {
DispatchQueue.global(qos: .userInteractive).async {
Thread.current.name = #function
selection.forEach{ idToRemove in
if let index = allElements.firstIndex(where: { $0.id == idToRemove }) {
allElements.remove(at: index)
}
}
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var delInfoButton: some View {
if editMode == .inactive {
return Button(action: {}) {
Image(systemName: "square.and.arrow.up")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
edit = .inactive
edit = .active
UPDATE
with Asperi's solution, I lose the propriety of LazyVStack, all the rows are loaded also if not displayed (and is also not scrollable:
struct SampleRow: View {
let number: Int
var body: some View {
Text("Sel Row \(number)")
}
init(_ number: Int) {
print("Loading LazySampleRow row \(number)")
self.number = number
}
}
struct LazySampleRow: View {
let number: Int
var body: some View {
Text("LVS element \(number)")
}
init(_ number: Int) {
print("Loading LazyVStack row \(number)")
self.number = number
}
}
var aLotOfElements: [MyElement] {
var temp: [MyElement] = []
for i in 1..<200 {
temp.append(MyElement(id: UUID(), number: i))
}
return temp
}
struct ContentView: View {
#State private var editMode: EditMode = .inactive
#State private var selection = Set<UUID>()
#State private var allElements: [MyElement] = aLotOfElements//[MyElement(id: UUID(), number: 1)]
var body: some View {
NavigationView {
HStack {
VStack {
Text("LazyVStack")
.foregroundColor(.red)
ScrollView {
LazyVStack (alignment: .leading) {
ForEach(allElements, id: \.self) { element in //section data
LazySampleRow(element.number)
}
}
}
}
Divider()
VStack {
LazyVStack (alignment: .leading) {
Divider()
Text("LazyVStackSelectionable")
.foregroundColor(.red)
LazyVStackSelectionable(allElements, selection: $selection) { element in
SampleRow(element.number)
}
Divider()
}
}
}
.environment(\.editMode, self.$editMode)
.navigationBarTitle(Text("LIST"), displayMode: .inline)
.navigationBarItems(//EDIT
trailing:
Group {
HStack (spacing: 15) {
self.editButton
self.delInfoButton
.contentShape(Rectangle())
}
}
)
}
}
//MARK: EDIT MODE
private func deleteItems() {
DispatchQueue.global(qos: .userInteractive).async {
Thread.current.name = #function
selection.forEach{ idToRemove in
if let index = allElements.firstIndex(where: { $0.id == idToRemove }) {
allElements.remove(at: index)
}
}
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var delInfoButton: some View {
if editMode == .inactive {
return Button(action: {}) {
Image(systemName: "square.and.arrow.up")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension EditMode {
var title: String {
self == .active ? NSLocalizedString("done", comment: "") : NSLocalizedString("edit", comment: "")
}
mutating func toggle() {
self = self == .active ? .inactive : .active
}
}
You need to create custom handled containers for all variants of desired content types.
Below is a demo of possible direction on the example of following content support (by example of List)
LazyVStackSelectionable(allElements, selection: $selection) { element in
Text(element.text)
}
Demo prepared and tested with Xcode 12 / iOS 14 (it is used some SwiftUI 2.0 features so if needed SwiftUI 1.0 support some more tuning will be needed)
struct LazyVStackSelectionable<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {
#Environment(\.editMode) var editMode
private var selection: Binding<Set<SelectionValue>>?
private var content: () -> Content
private var editingView: AnyView?
init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: #escaping () -> Content)
{
self.selection = selection
self.content = content
}
var body: some View {
Group {
if editingView != nil && self.editMode?.wrappedValue == .active {
editingView!
} else {
self.content()
}}
}
}
extension LazyVStackSelectionable {
init<Data, RowContent>(_ data: Data, selection: Binding<Set<SelectionValue>>?, #ViewBuilder rowContent: #escaping (Data.Element) -> RowContent) where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>, Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable, SelectionValue == Data.Element.ID
{
self.init(selection: selection, content: {
ForEach(data) { el in
HStack {
rowContent(el)
}
}
})
editingView = AnyView(
ForEach(data) { el in
HStack {
rowContent(el)
if let selection = selection {
Button(action: {
if selection.wrappedValue.contains(el.id) {
selection.wrappedValue.remove(el.id)
} else {
selection.wrappedValue.insert(el.id)
}
}) {
Image(systemName: selection.wrappedValue.contains(el.id) ? "checkmark.circle.fill" : "circle")
}
}
}
}
)
}
}
Instead of creating custom LazyVStack I suggest to modify ContentView and pass bindings to it.
struct SampleRow: View {
let element: MyElement
let editMode: Binding<EditMode>
let selection: Binding<Set<UUID>>?
var body: some View {
HStack {
if editMode.wrappedValue == .active,
let selection = selection {
Button(action: {
if selection.wrappedValue.contains(element.id) {
selection.wrappedValue.remove(element.id)
} else {
selection.wrappedValue.insert(element.id)
}
}) {
Image(systemName: selection.wrappedValue.contains(element.id) ? "checkmark.circle.fill" : "circle")
}
}
Text("Sel Row \(element.number)")
}
}
init(_ element: MyElement,
editMode: Binding<EditMode>,
selection: Binding<Set<UUID>>?) {
print("Loading LazySampleRow row \(element.number)")
self.editMode = editMode
self.element = element
self.selection = selection
}
}
And then you can just wrap normal LazyVStack in ScrollView to achieve what you need.
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(allElements, id: \.self) {
SampleRow($0,
editMode: $editMode,
selection: $selection)
}
}
}

What is the best way to switch views in SwiftUI?

I have tried several options to switch views in SwiftUI. However, each one had issues like lagging over time when switching back and forth many times. I am trying to find the best and cleanest way to switch views using SwiftUI. I am just trying to make a multiview user interface.
In View1.swift:
import SwiftUI
struct View1: View {
#State var GoToView2:Bool = false
var body: some View {
ZStack {
if (GoToView2) {
View2()
//What should I do if I created another swiftui view under the name View2?
//Just calling View2() like that causes lag as described in the linked question before it was deleted, if from view2 I switch back to view1 and so on.
//If I directly put the code of View2 here, then adding other views would get too messy.
} else {
VStack {
Button(action: {self.GoToView2.toggle()}) {
Text("Go to view 2")
}
}
}
}
}
}
In View2.swift:
import SwiftUI
struct View2: View {
#State var GoToView1:Bool = false
var body: some View {
ZStack {
if (GoToView1) {
View1()
} else {
VStack {
Button(action: {self.GoToView1.toggle()}) {
Text("Go to view 1")
}
}
}
}
}
}
I hope the problem can be understood. To replicate the behavior, please compile the code in a SwiftUI app, then switch be repeatedly switching between the two buttons quickly for 30 seconds, then you should notice a delay between each switch, and resizing the window should look chunky. I am using the latest version of macOS and the latest version of Xcode.
So I tried to show that each of the calls to the Views would add an instance to the view stack... I might be wrong here but the following should show this:
struct View1: View {
#State var GoToView2:Bool = false
var counter: Int
init(counter: Int) {
self.counter = counter + 1
}
var body: some View {
VStack {
if (GoToView2) {
Text("\(self.counter)")
View2(counter: self.counter)
} else {
VStack {
Button(action: {
withAnimation {
self.GoToView2.toggle()
}
}) {
Text("Go to view 2")
}
}
}
}
}
}
struct View2: View {
#State var GoToView1:Bool = false
var counter: Int
init(counter: Int) {
self.counter = counter + 1
}
var body: some View {
VStack {
if (GoToView1) {
Text("\(self.counter)")
View1(counter: self.counter)
} else {
VStack {
Button(action: {
withAnimation {
self.GoToView1.toggle()
}
}) {
Text("Go to view 1")
}
}.transition(.move(edge: .leading))
}
}
}
}
The I tried to show that the other method wouldn't do that:
struct View1: View {
#State var GoToView2: Bool = false
var counter: Int
init(counter: Int) {
self.counter = counter + 1
}
var body: some View {
VStack {
if (GoToView2) {
Text("\(self.counter)")
View2(counter: self.counter, GoToView1: self.$GoToView2)
} else {
VStack {
Button(action: {
withAnimation {
self.GoToView2.toggle()
}
}) {
Text("Go to view 2")
}
}
}
}
}
}
struct View2: View {
#Binding var GoToView1: Bool
var counter: Int
init(counter: Int, GoToView1: Binding<Bool>) {
self._GoToView1 = GoToView1
self.counter = counter + 1
}
var body: some View {
VStack {
Text("\(self.counter)")
Button(action: {
withAnimation {
self.GoToView1.toggle()
}
}) {
Text("Go to view 1")
}
}.transition(.move(edge: .leading))
}
}
I don't know if the lag is really coming from this or if there is a better method of proof, but for now this is what I came up with.
Original answer
I would recommend doing the following:
struct View1: View {
#State var GoToView2:Bool = false
var body: some View {
ZStack {
if (GoToView2) {
View2(GoToView1: self.$GoToView2)
} else {
VStack {
Button(action: {
withAnimation {
self.GoToView2.toggle()
}
}) {
Text("Go to view 2")
}
}
}
}
}
}
struct View2: View {
#Binding var GoToView1: Bool
var body: some View {
VStack {
Button(action: {
withAnimation {
self.GoToView1.toggle()
}
}) {
Text("Go to view 1")
}
}.transition(.move(edge: .leading))
}
}