SwiftUI: changing view using "if else" - swift

I’m trying to change a view based on a selection made with a button. I’m using Combine and SwiftUI.
I create a view router class with a #Published var:
import Foundation
import SwiftUI
import Combine
class ViewRouter: ObservableObject {
#Published var currentView = "folder"
}
I have my button struct:
import SwiftUI
import Combine
struct MultiButton: View {
#ObservedObject var viewRouter = ViewRouter()
#State var showButton = false
var body: some View {
GeometryReader { geometry in
ZStack{
// open multi button
Button(action: {
withAnimation {
self.showButton.toggle()
}
}) {
Image(systemName: "gear")
.resizable()
.frame(width: 40, height: 40, alignment: .center)
}
if self.showButton {
Multi()
.offset(CGSize(width: -30, height: -100))
}
}
}
}
}
struct MultiButton_Previews: PreviewProvider {
static var previews: some View {
MultiButton()
}
}
struct Multi: View {
#ObservedObject var viewRouter = ViewRouter()
var body: some View {
HStack(spacing: 10) {
ZStack {
Button(action: {
self.viewRouter.currentView = "folder"
debugPrint("\(self.viewRouter.currentView)")
}) {
ZStack{
Circle()
.foregroundColor(Color.blue)
.frame(width: 70, height: 70)
Image(systemName: "folder")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(20)
.frame(width: 70, height: 70)
.foregroundColor(.white)
}.shadow(radius: 10)
}
}
Button(action: {
self.viewRouter.currentView = "setting"
debugPrint("\(self.viewRouter.currentView)")
}, label: {
ZStack {
Circle()
.foregroundColor(Color.blue)
.frame(width: 70, height: 70)
Image(systemName: "gear")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(20)
.frame(width: 70, height: 70)
.foregroundColor(.white)
}.shadow(radius: 10)
})
}
.transition(.scale)
}
}
And the contentView:
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var viewRouter = ViewRouter()
var body: some View {
VStack{
MultiButton()
if viewRouter.currentView == "folder" {
Text("folder")
} else if viewRouter.currentView == "setting"{
Text("setting")
}
}
}
}
Why is my text is not changing from the folder to setting the base when the button is pressEd?
The var is #Published; it should publish the change and update the main view.
Did I miss something?

You have multiple instances of ViewRouter with each view having its own instance. If you change that instance, the other instances are not going to change.
To solve this, you either have to pass one instance around manually:
struct MultiButton {
#ObservedObject var viewRouter: ViewRouter
...
}
struct ContentView {
#ObservedObject var viewRouter = ViewRouter()
var body: some View {
VStack {
MultiButton(viewRouter: self.viewRouter)
...
}
}
}
Or you can declare it as an environment object:
struct MultiButton {
#EnvironmentObject var viewRouter: ViewRouter
...
}
And then inject it for example when you create your view hierarchy in your scene delegate:
let contentView = ContentView()
.environmentObject(ViewRouter())
Note that when injecting the environment object in the scene delegate, it will not be available in every preview of a view that uses the ViewRouter, unless you manually inject it there as well:
static var previews: some View {
ContentView()
.environmentObject(ViewRouter())
}

Related

SwiftUI .overlay() causing data to be displayed incorrectly

I'm working on an app that calls an api and displays the data on 6 different cards. In the card view the data displays correctly. Once I place that view into another using .overlay() the UI is perfect but the data on each card is the same. Without .overlay() the data is correct but the UI is wrong. I've tried changing my ViewModel variable in the views to #EnvironmentObject, #StateObject, & #ObservedObject with no luck. I can't figure out if this is a bug or I am missing something. Any help is greatly appreciated.
This view displays data correctly:
import SwiftUI
struct TopActivityCardView: View {
// #EnvironmentObject var vm: ViewModel
#StateObject var vm = ViewModel()
var timePeriod = ""
var body: some View {
ZStack {
ScrollView{
ForEach(vm.fetch(), id: \.title) { item in
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.darkBlue)
.frame(height: 120, alignment: .center)
.padding(.top, 80)
.overlay(
ActivityTitleView(activityTitle: item.title)
).overlay(
ActivityHoursView(workHours: item.timeframes.weekly.current)
).overlay(
ActivityEllipisView()
).overlay(
TimePeriodView(timePeriod: "Last Week", weeklyHrs:
item.timeframes.weekly.previous)
)
}
}
}
}
}
struct TopActivityCardView_Previews: PreviewProvider {
static var previews: some View {
TopActivityCardView()
.environmentObject(ViewModel())
}
}
This view is where the problem occurs:
import SwiftUI
struct RectCardView: View {
var colorArray: [(colorName: Color, imageName: String)] = [(Color.work, "icon-work"),
(Color.play, "icon-play"), (Color.study, "icon-study"), (Color.exercise, "icon-
exercise"), (Color.social, "icon-social"), (Color.selfCare, "icon-self-care")]
// #StateObject var vm = ViewModel()
#ObservedObject var vm = ViewModel()
var body: some View {
ZStack{
ScrollView{
ForEach(colorArray, id: \.imageName) { colorName, imageName in
RoundedRectangle(cornerRadius: 20)
.foregroundColor(colorName)
.frame(height: 110, alignment: .center)
.overlay(
Image(imageName)
.brightness(-0.2)
.frame(width: 60, height: 20, alignment: .topLeading)
.offset(x: 103, y: -60)
).clipped()
/// If I take .overlay() off the cards display correct data but UI is
not correct.
/// With overlay UI is correct but data on cards is all the same.
.overlay{
TopActivityCardView()
.padding(.top, -40)
}
.padding(.top, 20)
}
}
}
}
}
struct RectCardView_Previews: PreviewProvider {
static var previews: some View {`enter code here`
RectCardView()
.environmentObject(ViewModel())
}
}
In TopActivityCardView, you are making a new view model (#StateObject var vm = ViewModel()).
It isn't using the same view model as RectCardView, so the data is different.
You could pass the view into TopActivityCardView by using an environment object:
// code…
struct TopActivityCardView: View {
#EnvironmentObject var vm: ViewModel
// more code…
}
.overlay{
TopActivityCardView()
.environmentObject(vm)
.padding(.top, -40)
}

Move From SpriteKit scene to SwiftUI View

I'm trying to figure out the proper way to move back to a SwiftUI view from a SpriteKit Scene. I currently have a Swift UI "main menu" which looks like this.
struct MainMenu: View {
var body: some View {
NavigationView {
VStack {
Text("Replicator")
.font(.largeTitle)
.fontWeight(.bold)
.padding()
NavigationLink(destination: ContentView().navigationBarBackButtonHidden(true)) {
HStack {
Image(systemName: "play.circle")
.font(.title3)
Text("Start")
.font(.title)
}
.frame(width: 250, height: 50)
.background(.cyan)
.cornerRadius(25)
.foregroundColor(.white)
}
}
}
}
}
The ContentView() is what contains the SpriteKit game and that looks like the following.
struct ContentView: View {
var scene: SKScene {
let scene = Level_1()
scene.size = CGSize(width: 750, height: 1334)
scene.scaleMode = .aspectFit
return scene
}
var body: some View {
VStack {
SpriteView(scene: scene)
.ignoresSafeArea()
}
}
}
My question is... once I'm in ContentView how do I return to "Main Menu"?
Thanks for any help you can provide.
You can use
#Environment(.presentationMode) var presentationMode
So you can create a button and call
presentationMode.wrappedValue.dismiss()
Or you can pass a binding var to Content View and set to false like so:
In MainMenu
struct MainMenu: View {
#State var isPresented = false
var body: some View {
NavigationView {
VStack {
Text("Replicator")
.font(.largeTitle)
.fontWeight(.bold)
.padding()
NavigationLink(destination: ContentView(isPresented: $isPresented).navigationBarBackButtonHidden(true), isActive: $isPresented) {
HStack {
Image(systemName: "play.circle")
.font(.title3)
Text("Start")
.font(.title)
}
.frame(width: 250, height: 50)
.background(.cyan)
.cornerRadius(25)
.foregroundColor(.white)
}
}
}
}
}
In ContentView:
struct ContentView: View {
#Binding var isPresented: Bool
var scene: SKScene {
let scene = Level_1()
scene.size = CGSize(width: 750, height: 1334)
scene.scaleMode = .aspectFit
return scene
}
var body: some View {
ZStack {
SpriteView(scene: scene)
.ignoresSafeArea()
Button(action: { //You can put the button wherever you want as long as you pass in the isPresented Binding
isPresented.toggle()
}) {
Text("Back to MainMenu")
}
}
}
}

SwiftUI each item displays it own detailView when it is selected in a list

I'm trying to make each item display it own detailView in a list using SwiftUI. But for now, I got stuck because it only display the same detailView for any item. Would anyone know how to do that?
This is the code I have for now:
struct PastryListView: View {
#State private var isShowingDetailView = false
#State private var selectedPastry : Pastry?
#State private var selection: Int? = nil
var body: some View {
ZStack {
NavigationView {
List(MockData.pastries) { Pastry in
HStack {
Image(Pastry.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 180, height: 200)
VStack {
Text(Pastry.name)
.font(Font.custom("DancingScript-Regular", size: 30))
.fontWeight(.medium)
}
.padding(.leading)
}
.onTapGesture {
selectedPastry = Pastry
isShowingDetailView = true
}
}
.navigationTitle("🥐 Pastries")
}
if isShowingDetailView { Pastry2DetailView(isShowingDetailView2: $isShowingDetailView, pastry: MockData.samplePastry2)
}
}
} }
there are many ways to achieve what you want, this is just one way:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
PastryListView()
}
}
}
struct Pastry: Identifiable {
var id: String = UUID().uuidString
var name: String
var image: UIImage
}
struct PastryListView: View {
#State private var pastries: [Pastry] = [
Pastry(name: "pastry1", image: UIImage(systemName: "globe")!),
Pastry(name: "pastry2", image: UIImage(systemName: "info")!)]
var body: some View {
NavigationView {
List(pastries) { pastry in
NavigationLink(destination: PastryDetailView(pastry: pastry)) {
HStack {
Image(uiImage: pastry.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 80, height: 80)
VStack {
Text(pastry.name)
.font(Font.custom("DancingScript-Regular", size: 30))
.fontWeight(.medium)
}
.padding(.leading)
}
}
}.navigationTitle("🥐 Pastries")
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct PastryDetailView: View {
#State var pastry: Pastry
var body: some View {
Text("🥐🥐🥐🥐 " + pastry.name + " 🥐🥐🥐🥐")
}
}

Why my ObservableObject seems to be broken with a specific view?

I have created a class for keeping the trace a the actual view (a simple ObservableObject String) :
import Foundation
import SwiftUI
import Combine
class ViewRouter: ObservableObject {
#Published var currentView: String
init() {
self.currentView = "Home"
}
}
I have create a simple custom TabView and it works very well with this :
import SwiftUI
struct Test2CustomTabView: View {
#ObservedObject var viewRouter = ViewRouter()
var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
if self.viewRouter.currentView == "Home" {
Home()
} else if self.viewRouter.currentView == "Messages" {
Messages()
}
Spacer()
HStack {
VStack {
Image(systemName: "house")
Text("Home")
.font(.caption)
}.frame(width: geometry.size.width/2, alignment: .center)
.offset(y: -15)
.onTapGesture {
self.viewRouter.currentView = "Home"
}
VStack {
Image(systemName: "envelope")
Text("Messages")
.font(.caption)
}.frame(width: geometry.size.width/2, alignment: .center)
.offset(y: -15)
.onTapGesture {
self.viewRouter.currentView = "Messages"
}
}.frame(height: 85)
.foregroundColor(.white)
.background(Color(#colorLiteral(red: 0.259467423, green: 0.5342320204, blue: 0.7349982858, alpha: 1))) // On utilise ici extension Color plus bas
}
}
.edgesIgnoringSafeArea(.bottom)
}
}
I want to simplify the code so i created this view :
import SwiftUI
struct Test2CustomTabViewItem: View {
#ObservedObject var viewRouter = ViewRouter()
var systemName:String
var viewName:String
let screenSize: CGRect = UIScreen.main.bounds
var body: some View {
VStack {
Image(systemName: systemName)
Text("Home")
.font(.caption)
}.frame(width: screenSize.width/2, alignment: .center)
.offset(y: -15)
.onTapGesture {
self.viewRouter.currentView = self.viewName
}
}
}
struct Test2CustomTabViewItem_Previews: PreviewProvider {
static var previews: some View {
Test2CustomTabViewItem(systemName: "house", viewName: "Home")
}
}
And now im using the Test2CustomTabView like this :
import SwiftUI
struct Test2CustomTabView: View {
#ObservedObject var viewRouter = ViewRouter()
var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
if self.viewRouter.currentView == "Home" {
Home()
} else if self.viewRouter.currentView == "Messages" {
Messages()
}
Spacer()
HStack {
Test2CustomTabViewItem(systemName: "house", viewName: "Home")
Test2CustomTabViewItem(systemName: "envelope", viewName: "Messages")
}.frame(height: 85)
.foregroundColor(.white)
.background(Color(#colorLiteral(red: 0.259467423, green: 0.5342320204, blue: 0.7349982858, alpha: 1))) // On utilise ici extension Color plus bas
}
}
.edgesIgnoringSafeArea(.bottom)
}
}
struct Test2CustomTabView_Previews: PreviewProvider {
static var previews: some View {
Test2CustomTabView()
}
}
But now when i click on the custom tabview item the view are not refreshed, the change in the ObservableObject doesn't refresh the view and i dont understand why..
Any idea?
Thanks
In your code each view has his own ViewRouter instance. Pass it to Test2CustomTabViewItem view from the constructor or as environmentObject:
struct Test2CustomTabViewItem: View {
#ObservedObject var viewRouter: ViewRouter
var systemName:String
var viewName:String
...
}
struct Test2CustomTabView: View {
#ObservedObject var viewRouter = ViewRouter()
...
Test2CustomTabViewItem(viewRouter: self.viewRouter, systemName: "house", viewName: "Home")
Test2CustomTabViewItem(viewRouter: self.viewRouter, systemName: "envelope", viewName: "Messages")
...
}

How do I implement #EnvironmentObject for this custom full-screen modal setup?

My goal is to have custom modals present over an root view that is essentially a tabbed view. So, I wrapped the TabView in a ZStack and am using an ObservableOBject. But I don't feel I'm doing it the right way.
In my other file, I have the Custom modal "subviews" which has an enum, too, which I think is the right approach to take. But I cannot figure out how to dismiss a modal after it is visible.
It must be #EnvironmentObject, but I don't know what if anything to put in the scene delegate, etc. ("Hacking with Swift" is failing me here, although it's a great resource.)
My idea is that views from the tabbed view will have various buttons which present different modal views, populated later with data specific to say a user and set of fields for data entry.
Right now, I just want to understand how to present and dismiss them.
Here is my root view
import SwiftUI
struct ContentView: View {
#ObservedObject var modal = CustomModal()
var body: some View {
ZStack {
TabView {
ZStack {
Color.pink.opacity(0.2)
Button(action: {
withAnimation{
self.modal.visibleModal = VisibleModal.circle
}
}) {
Text("Circle").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.pink.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "1.square.fill")
Text("One")
}
}.tag(1)
ZStack {
Color.blue.opacity(0.2)
Button(action: {
self.modal.visibleModal = VisibleModal.squircle
}) {
Text("Square").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.blue.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "2.square.fill")
Text("Two")
}
}.tag(2)
}.accentColor(.purple)
VStack {
containedView()
}
}
}
func containedView() -> AnyView {
switch modal.visibleModal {
case .circle: return AnyView(CircleView())
case .squircle: return AnyView(SquircleView())
case .none: return AnyView(Text(""))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And here is my second file with the enum and "subview" modals
import SwiftUI
class CustomModal: ObservableObject {
#Published var visibleModal: VisibleModal = VisibleModal.none
}
enum VisibleModal {
case circle, squircle, none
}
struct CircleView: View {
var body: some View {
ZStack {
Color.pink.blur(radius: 0.4)
Circle().fill()
.frame(width: 300)
.foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct SquircleView: View {
var body: some View {
ZStack{
Color.green.blur(radius: 0.4)
RoundedRectangle(cornerRadius: 48, style: .continuous)
.frame(width: 300, height: 300).foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct dismissButton: View {
#ObservedObject var modal = CustomModal()
var body: some View {
VStack{
Spacer()
Button(action: {
self.modal.visibleModal = VisibleModal.none
}) {
Text("Dismiss").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.white.opacity(0.35)).foregroundColor(.white)
.cornerRadius(12)
.padding(.bottom, 44)
}
}
}
Are you just trying to pass your observable object to the new view?
func containedView() -> some View {
switch modal.visibleModal {
case .circle: return CircleView()
.environmentObject(self.modal)
case .squircle: return SquircleView()
.environmentObject(self.modal)
case .none: return Text("")
}
}
Unless I am misunderstanding the question.
Okay, after a lot of fiddling, it works.
Now my code is as follows.
Root view
struct ContentView: View {
#EnvironmentObject var isModalVisible: CustomModal
#ObservedObject var modal = CustomModal()
var body: some View {
ZStack {
TabView {
ZStack {
Color.pink.opacity(0.2)
Button(action: {
withAnimation{
self.isModalVisible.isModalVisible.toggle()
self.modal.currentModal = VisibleModal.circle
}
}) {
Text("Circle").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.pink.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "1.square.fill")
Text("One")
}
}.tag(1)
ZStack {
Color.blue.opacity(0.2)
Button(action: {
self.isModalVisible.isModalVisible.toggle()
self.modal.currentModal = VisibleModal.squircle
}) {
Text("Square").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.blue.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "2.square.fill")
Text("Two")
}
}.tag(2)
}.accentColor(.purple)
if self.isModalVisible.isModalVisible {
VStack {
containedView()
}
}
}
}
func containedView() -> AnyView {
switch modal.currentModal {
case .circle: return AnyView(CircleView())
case .squircle: return AnyView(SquircleView())
case .none: return AnyView(Text(""))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(CustomModal())
}
}
and the second file with the supporting views and classes and enums:
import SwiftUI
class CustomModal: ObservableObject {
#Published var isModalVisible = false
#Published var currentModal: VisibleModal = .none
}
enum VisibleModal {
case circle, squircle, none
}
struct CircleView: View {
#EnvironmentObject var env: CustomModal
var body: some View {
ZStack {
Color.pink.blur(radius: 0.4)
Circle().fill()
.frame(width: 300)
.foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct SquircleView: View {
var body: some View {
ZStack{
Color.green.blur(radius: 0.4)
RoundedRectangle(cornerRadius: 48, style: .continuous)
.frame(width: 300, height: 300).foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct dismissButton: View {
#EnvironmentObject var env: CustomModal
var body: some View {
VStack{
Spacer()
Button(action: {
self.env.isModalVisible.toggle()
print("TAPPED")
}) {
Text("Dismiss").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.white.opacity(0.35)).foregroundColor(.white)
.cornerRadius(12)
.padding(.bottom, 44)
}
}
}
It still can be refactored. I'm sure. I'd also be happy to hear any comments on how to improve it. But it seems to work.
NOTE: This code ContentView().environmentObject(CustomModal()) is put in the previewP{rovider code and in SceneDelegate.