SwiftUI, assign view to variable - swift

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.

Related

Intercept custom action on TabView

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
}
}
}
}

SwiftUI - Not changing views

I have a form that I want to change views from if a Bool is false:
var body: some View {
NavigationView {
Form {
Section {
Toggle(isOn: $ClientAnswer) {
Text("Client answer")
}
if ClientAnswer {
Toggle(isOn: $submission.fieldOne ) {
Text("Field One")
}
Toggle(isOn: $submission.fieldTwo ) {
Text("Field Two")
}
}
}
Section {
Button(action: {
if self.ClientAnswer{
self.placeSubmission()
}
else {
ShowRS() //**change views here.**
print("test")
}
}){
Text("Submit")
}
}.disabled(!submission.isValid)
}
}
}
The code is being executed as print("test") works, but it doesn't change view it just stays on the same view?
The view I am trying to switch to is:
struct ShowRS: View {
var body: some View {
Image("testImage")
}
}
You have to include ShowRS in your view hierarchy -- right now, it's just in your Buttons action callback. There are multiple ways to achieve this, but here's one option:
struct ContentView : View {
#State private var moveToShowRSView = false
var body: some View {
NavigationView {
Button(action: {
if true {
moveToShowRSView = true
}
}) {
Text("Move")
}.overlay(NavigationLink(destination: ShowRS(), isActive: $moveToShowRSView, label: {
EmptyView()
}))
}
}
}
struct ShowRS: View {
var body: some View {
Image("testImage")
}
}
I've simplified this down from your example since I didn't have all of your code with your models, etc, but it demonstrates the concept. In the Button action, you test a boolean (in this case, it'll always return true) and then set the #State variable moveToShowRSView.
If moveToShowRSView is true, there's an overlay on the Button that has a NavigationLink (which is invisible because of the EmptyView) which will only be active if moveToShowRSView is true

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))
}
}

How to create a custom TabView with NavigationView in SwiftUI?

[EDIT] - This question has been edited and simplified.
I need to create a CustomLooking TabView instead of the default one.
Here is my full code with the problem. Just run the code below.
import SwiftUI
enum TabName {
case explore, network
}
struct ContentView: View {
#State var displayedTab: TabName = .explore
var body: some View {
VStack(spacing: 0) {
Spacer()
switch displayedTab {
case .explore: AViewWhichNavigates(title: "Explore").background(Color.yellow)
case .network: AViewWhichNavigates(title: "Network").background(Color.green)
}
Spacer()
CustomTabView(displayedTab: $displayedTab)
}
}
}
struct CustomTabView: View {
#Binding var displayedTab: TabName
var body: some View {
HStack {
Spacer()
Text("Explore").border(Color.black, width: 1).onTapGesture { self.displayedTab = .explore }
Spacer()
Text("Network").border(Color.black, width: 1).onTapGesture { self.displayedTab = .network }
Spacer()
}
}
}
struct AViewWhichNavigates: View {
let title: String
var body: some View {
NavigationView(content: {
NavigationLink(destination: Text("We are one level deep in navigation")) {
Text("You are at root. Tap to navigate").navigationTitle(title)
}
})
}
}
On tab#1 click the navigation. Switch to tab#2, then Switch back to tab#1. You will see that tab#1 has popped to root.
How do I prevent the customTabView from popping to root every time i switch tabs?
All you need is a ZStack with opacity.
import SwiftUI
enum TabName {
case explore, network
}
struct ContentView: View {
#State var displayedTab: TabName = .explore
var body: some View {
VStack {
ZStack {
AViewWhichNavigates(title: "Explore")
.background(Color.green)
.opacity(displayedTab == .explore ? 1 : 0)
AViewWhichNavigates(title: "Network")
.background(Color.green)
.opacity(displayedTab == .network ? 1 : 0)
}
CustomTabView(displayedTab: $displayedTab)
}
}
}
struct CustomTabView: View {
#Binding var displayedTab: TabName
var body: some View {
HStack {
Spacer()
Text("Explore").border(Color.black, width: 1).onTapGesture { self.displayedTab = .explore }
Spacer()
Text("Network").border(Color.black, width: 1).onTapGesture { self.displayedTab = .network }
Spacer()
}
}
}
struct AViewWhichNavigates: View {
let title: String
var body: some View {
NavigationView(content: {
NavigationLink(destination: Text("We are one level deep in navigation")) {
Text("You are at root. Tap to navigate").navigationTitle(title)
}
})
}
}
The problem is that the Navigation isActive state is not recorded as well as the displayed tab state.
By recording the state of the navigation of each tab as well as which tab is active the correct navigation state can be show for each tab.
The model can be improved to remove the tuple and make it more flexible but the key thing is the use of getter and setter to use an encapsulated model of what the navigation state is for each tab in order to allow the NavigationLink to update it via a binding.
I have simplified the top level VStack and removed the top level switch as its not needed here, but it can be added back for using different types of views at the top level in a real implementation
enum TabName : String {
case Explore, Network
}
struct ContentView: View {
#State var model = TabModel()
init(){
UINavigationBar.setAnimationsEnabled(false)
}
var body: some View {
VStack(spacing: 0) {
Spacer()
AViewWhichNavigates(model: $model).background(Color.green)
Spacer()
CustomTabView(model:$model)
}
}
}
struct CustomTabView: View {
#Binding var model: TabModel
var body: some View {
HStack {
Spacer()
Text("Explore").border(Color.black, width: 1).onTapGesture { model.selectedTab = .Explore }
Spacer()
Text("Network").border(Color.black, width: 1).onTapGesture { model.selectedTab = .Network }
Spacer()
}
}
}
struct AViewWhichNavigates: View {
#Binding var model:TabModel
var body: some View {
NavigationView(content: {
NavigationLink(destination: Text("We are one level deep in navigation in \(model.selectedTab.rawValue)"), isActive: $model.isActive) {
Text("You are at root of \(model.selectedTab.rawValue). Tap to navigate").navigationTitle(model.selectedTab.rawValue)
}.onDisappear {
UINavigationBar.setAnimationsEnabled(model.isActive)
}
})
}
}
struct TabModel {
var selectedTab:TabName = .Explore
var isActive : Bool {
get {
switch selectedTab {
case .Explore : return tabMap.0
case .Network : return tabMap.1
}
}
set {
switch selectedTab {
case .Explore : nOn(isActive, newValue); tabMap.0 = newValue;
case .Network : nOn(isActive, newValue); tabMap.1 = newValue;
}
}
}
//tuple used to represent a fixed set of tab isActive navigation states
var tabMap = (false, false)
func nOn(_ old:Bool,_ new:Bool ){
UINavigationBar.setAnimationsEnabled(new && !old)
}
}
I think it is possible even with your custom tab view, because the issue is in rebuilding ExploreTab() when you switch tabs, so all content of that tab is rebuilt as well, so internal NavigationView on rebuilt is on first page.
Assuming you have only one ExploreTab in your app (as should be obvious), the possible solution is to make it Equatable explicitly and do not allow SwiftUI to replace it on refresh.
So
struct ExploreTab: View, Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
return true // prevent replacing ever !!
}
var body: some View {
// ... your code here
}
}
and
VStack(spacing: 0) {
switch displayedTab {
case .explore: ExploreTab().equatable() // << here !!
case .network: NetworkTab()
}
CustomTabView(displayedTab: $displayedTab) //This is the Custom TabBar
}
Update: tested with Xcode 12 / iOS 14 - works as described above (actually the same idea works for standard containers)
Here is a quick demo replication of CustomTabView with test environment as described above.
Full module code:
struct ExploreTab: View, Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
return true // prevent replacing ever !!
}
var body: some View {
NavigationView {
NavigationLink("Go", destination: Text("Explore"))
}
}
}
enum TestTabs {
case explore
case network
}
struct CustomTabView: View {
#Binding var displayedTab: TestTabs
var body: some View {
HStack {
Button("Explore") { displayedTab = .explore }
Divider()
Button("Network") { displayedTab = .network }
}
.frame(maxWidth: .infinity)
.frame(height: 80).background(Color.yellow)
}
}
struct TestCustomTabView: View {
#State private var displayedTab = TestTabs.explore
var body: some View {
VStack(spacing: 0) {
switch displayedTab {
case .explore: ExploreTab().equatable() // << here !!
case .network: Text("NetworkTab").frame(maxWidth: .infinity, maxHeight: .infinity)
}
CustomTabView(displayedTab: $displayedTab) //This is the Custom TabBar
}
.edgesIgnoringSafeArea(.bottom)
}
}

SwiftUI: Support multiple modals

I'm trying to setup a view that can display multiple modals depending on which button is tapped.
When I add just one sheet, everything works:
.sheet(isPresented: $showingModal1) { ... }
But when I add another sheet, only the last one works.
.sheet(isPresented: $showingModal1) { ... }
.sheet(isPresented: $showingModal2) { ... }
UPDATE
I tried to get this working, but I'm not sure how to declare the type for modal. I'm getting an error of Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
struct ContentView: View {
#State var modal: View?
var body: some View {
VStack {
Button(action: {
self.modal = ModalContentView1()
}) {
Text("Show Modal 1")
}
Button(action: {
self.modal = ModalContentView2()
}) {
Text("Show Modal 2")
}
}.sheet(item: self.$modal, content: { modal in
return modal
})
}
}
struct ModalContentView1: View {
var body: some View {
Text("Modal 1")
}
}
struct ModalContentView2: View {
var body: some View {
Text("Modal 2")
}
}
This works:
.background(EmptyView().sheet(isPresented: $showingModal1) { ... }
.background(EmptyView().sheet(isPresented: $showingModal2) { ... }))
Notice how these are nested backgrounds. Not two backgrounds one after the other.
Thanks to DevAndArtist for finding this.
Maybe I missed the point, but you can achieve it either with a single call to .sheet(), or multiple calls.:
Multiple .sheet() approach:
import SwiftUI
struct MultipleSheets: View {
#State private var sheet1 = false
#State private var sheet2 = false
#State private var sheet3 = false
var body: some View {
VStack {
Button(action: {
self.sheet1 = true
}, label: { Text("Show Modal #1") })
.sheet(isPresented: $sheet1, content: { Sheet1() })
Button(action: {
self.sheet2 = true
}, label: { Text("Show Modal #2") })
.sheet(isPresented: $sheet2, content: { Sheet2() })
Button(action: {
self.sheet3 = true
}, label: { Text("Show Modal #3") })
.sheet(isPresented: $sheet3, content: { Sheet3() })
}
}
}
struct Sheet1: View {
var body: some View {
Text("This is Sheet #1")
}
}
struct Sheet2: View {
var body: some View {
Text("This is Sheet #2")
}
}
struct Sheet3: View {
var body: some View {
Text("This is Sheet #3")
}
}
Single .sheet() approach:
struct MultipleSheets: View {
#State private var showModal = false
#State private var modalSelection = 1
var body: some View {
VStack {
Button(action: {
self.modalSelection = 1
self.showModal = true
}, label: { Text("Show Modal #1") })
Button(action: {
self.modalSelection = 2
self.showModal = true
}, label: { Text("Show Modal #2") })
Button(action: {
self.modalSelection = 3
self.showModal = true
}, label: { Text("Show Modal #3") })
}
.sheet(isPresented: $showModal, content: {
if self.modalSelection == 1 {
Sheet1()
}
if self.modalSelection == 2 {
Sheet2()
}
if self.modalSelection == 3 {
Sheet3()
}
})
}
}
struct Sheet1: View {
var body: some View {
Text("This is Sheet #1")
}
}
struct Sheet2: View {
var body: some View {
Text("This is Sheet #2")
}
}
struct Sheet3: View {
var body: some View {
Text("This is Sheet #3")
}
}
I'm not sure whether this was always possible, but in Xcode 11.3.1 there is an overload of .sheet() for exactly this use case (https://developer.apple.com/documentation/swiftui/view/3352792-sheet). You can call it with an Identifiable item instead of a bool:
struct ModalA: View {
var body: some View {
Text("Hello, World! (A)")
}
}
struct ModalB: View {
var body: some View {
Text("Hello, World! (B)")
}
}
struct MyContentView: View {
enum Sheet: Hashable, Identifiable {
case a
case b
var id: Int {
return self.hashValue
}
}
#State var activeSheet: Sheet? = nil
var body: some View {
VStack(spacing: 42) {
Button(action: {
self.activeSheet = .a
}) {
Text("Hello, World! (A)")
}
Button(action: {
self.activeSheet = .b
}) {
Text("Hello, World! (B)")
}
}
.sheet(item: $activeSheet) { item in
if item == .a {
ModalA()
} else if item == .b {
ModalB()
}
}
}
}
I personally would mimic some NavigationLink API. Then you can create a hashable enum and decide which modal sheet you want to present.
extension View {
func sheet<Content, Tag>(
tag: Tag,
selection: Binding<Tag?>,
content: #escaping () -> Content
) -> some View where Content: View, Tag: Hashable {
let binding = Binding(
get: {
selection.wrappedValue == tag
},
set: { isPresented in
if isPresented {
selection.wrappedValue = tag
} else {
selection.wrappedValue = .none
}
}
)
return background(EmptyView().sheet(isPresented: binding, content: content))
}
}
enum ActiveSheet: Hashable {
case first
case second
}
struct First: View {
var body: some View {
Text("frist")
}
}
struct Second: View {
var body: some View {
Text("second")
}
}
struct TestView: View {
#State
private var _activeSheet: ActiveSheet?
var body: some View {
print(_activeSheet as Any)
return VStack
{
Button("first") {
self._activeSheet = .first
}
Button("second") {
self._activeSheet = .second
}
}
.sheet(tag: .first, selection: $_activeSheet) {
First()
}
.sheet(tag: .second, selection: $_activeSheet) {
Second()
}
}
}
I wrote a library off plivesey's answer that greatly simplifies the syntax:
.multiSheet {
$0.sheet(isPresented: $sheetAPresented) { Text("Sheet A") }
$0.sheet(isPresented: $sheetBPresented) { Text("Sheet B") }
$0.sheet(isPresented: $sheetCPresented) { Text("Sheet C") }
}
I solved this by creating an observable SheetContext that holds and manages the state. I then only need a single context instance and can tell it to present any view as a sheet. I prefer this to the "active view" binding approach, since you can use this context in multiple ways.
I describe it in more details in this blog post: https://danielsaidi.com/blog/2020/06/06/swiftui-sheets
I think i found THE solution. It's complicated so here is the teaser how to use it:
Button(action: {
showModal.wrappedValue = ShowModal {
AnyView( TheViewYouWantToPresent() )
}
})
Now you can define at the button level what you want to present. And the presenting view does not need to know anything. So you call this on the presenting view.
.background(EmptyView().show($showModal))
We call it on the background so the main view does not need to get updated, when $showModal changes.
Ok so what do we need to get this to work?
1: The ShowModal class:
public enum ModalType{
case sheet, fullscreen
}
public struct ShowModal: Identifiable {
public let id = ""
public let modalType: ModalType
public let content: () -> AnyView
public init (modalType: ModalType = .sheet, #ViewBuilder content: #escaping () -> AnyView){
self.modalType = modalType
self.content = content
}
}
Ignore id we just need it for Identifiable. With modalType we can present the view as sheet or fullscreen. And content is the passed view, that will be shown in the modal.
2: A ShowModal binding which stores the information for presenting views:
#State var showModal: ShowModal? = nil
And we need to add it to the environment of the view thats responsible for presentation. So we have easy access to it down the viewstack:
VStack{
InnerViewsThatWantToPresentModalViews()
}
.environment(\.showModal, $showModal)
.background(EmptyView().show($showModal))
In the last line we call .show(). Which is responsible for presentation.
Keep in mind that you have to create #State var showModal and add it to the environment again in a view thats shown modal and wants to present another modal.
4: To use .show we need to extend view:
public extension View {
func show(_ modal: Binding<ShowModal?>) -> some View {
modifier(VM_Show(modal))
}
}
And add a viewModifier that handles the information passed in $showModal
public struct VM_Show: ViewModifier {
var modal: Binding<ShowModal?>
public init(_ modal: Binding<ShowModal?>) {
self.modal = modal
}
public func body(content: Content) -> some View {
guard let modalType = modal.wrappedValue?.modalType else{ return AnyView(content) }
switch modalType {
case .sheet:
return AnyView(
content.sheet(item: modal){ modal in
modal.content()
}
)
case .fullscreen:
return AnyView(
content.fullScreenCover(item: modal) { modal in
modal.content()
}
)
}
}
}
4: Last we need to set showModal in views that want to present a modal:
Get the variable with: #Environment(\.showModal) var showModal. And set it like this:
Button(action: {
showModal.wrappedValue = ShowModal(modalType: .fullscreen) {
AnyView( TheViewYouWantToPresent() )
}
})
In the view that defined $showModal you set it without wrappedValue: $showModal = ShowModal{...}
As an alternative, simply putting a clear pixel somewhere in your layout might work for you:
Color.clear.frame(width: 1, height: 1, alignment: .center).sheet(isPresented: $showMySheet, content: {
MySheetView();
})
Add as many pixels as necessary.