How to apply a different type of transition to a view in SwiftUI, depending on a conditional - swift

In my current project I have transitions applied to my authentication views. However, on sign in, I want a different transition to appear. (Different than if the user simply clicked the "back" button)
Here is some code for a basic mock up I made:
Auth View:
struct AuthView: View {
#EnvironmentObject var testModel: TestModel //Not used yet, used later
#State private var showSignIn: Bool = false
#State private var showSignUp: Bool = false
var body: some View {
if (!showSignIn && !showSignUp) {
WelcomeView(showSignIn: $showSignIn, showSignUp: $showSignUp)
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: .leading), removal: AnyTransition.move(edge: .leading)))
} else if showSignIn {
SignInView(showSignIn: $showSignIn)
.transition(AnyTransition.move(edge: .trailing))
} else {
SignUpView(showSignUp: $showSignUp)
.transition(AnyTransition.move(edge: .trailing))
}
}
}
Welcome View code:
struct WelcomeView: View {
#Binding var showSignIn: Bool
#Binding var showSignUp: Bool
var body: some View {
VStack {
Text("Welcome!")
Button(action: { withAnimation { showSignIn.toggle() } }) {
Text("Sign In")
.underline()
.foregroundColor(.white)
}
Button(action: { withAnimation { showSignUp.toggle() } }) {
Text("Sign Up")
.underline()
.foregroundColor(.white)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue.ignoresSafeArea())
}
}
Sign in view:
struct SignInView: View {
#EnvironmentObject var testModel: TestModel
#Binding var showSignIn: Bool
var body: some View {
VStack {
Text("Sign In")
Button(action: { withAnimation { showSignIn.toggle() } }) {
Text("Go back")
.underline()
.foregroundColor(.white)
}
Button(action: { withAnimation { testModel.signedIn.toggle() } }) {
Text("Complete Sign In")
.underline()
.foregroundColor(.white)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green.ignoresSafeArea())
}
}
ContentView:
struct ContentView: View {
#StateObject var testModel = TestModel()
var body: some View {
if testModel.signedIn {
Text("Home page")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
.transition(AnyTransition.opacity)
.onTapGesture {
testModel.signedIn.toggle()
}
} else {
AuthView()
.environmentObject(testModel)
}
}
}
What it makes:
What I want:
I'm trying to apply a different transition effect when I "finish" the sign in process. In my mock up, I simulated this through clicking "Complete sign in"
I tried changing my SignInView code (in my AuthView) to this, applying a ternary operator to the transitions:
SignInView(showSignIn: $showSignIn)
.transition(AnyTransition.move(edge: (testModel.signedIn ? .bottom : .trailing)))
But it had no effect.
Thanks for any help in advance!

I solved my own question!
I made a new #State variable, with a type of AnyTransition, using that variable in replacement of my ternary operator. I then used logic to change the type of transition (the #State variable) when I run my log-in function.

Related

Implementing Button in side menu

can someone Help me with fixing this. I want this code to work such as when I click the Home button on the side menu, it should take me to the Main View("This is the Main View"). I have tried using presenting sheets, however, presenting sheet doesn't look realistic. When the Home button is tapped, everything should disappear and only the Home Screen should come up with the side menu. I have tried writing up this code, however, I couldn't make the home button work. The codes are as below:
import SwiftUI
import Foundation
import Combine
struct Home: View {
#State var showMenu = false
#EnvironmentObject var userSettings: UserSettings
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.showMenu = false
}
}
}
return NavigationView {
GeometryReader {
geometry in
ZStack(alignment: .leading) {
MainView(showMenu: self.$showMenu)
.frame(width: geometry.size.width, height: geometry.size.height)
.offset(x: self.showMenu ? geometry.size.width/2 : 0)
.disabled(self.showMenu ? true : false)
if self.showMenu {
MenuView()
.frame(width: geometry.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
.navigationBarTitle("Pay Data", displayMode: .inline)
.navigationBarItems(leading: (Button(action: {
withAnimation {
self.showMenu.toggle()
}
}){
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}
))
}
}
}
struct MainView: View {
#Binding var showMenu: Bool
#EnvironmentObject var userSettings: UserSettings
var body: some View {
Text("This is Main View")
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
.environmentObject(UserSettings())
}
}
//This is the Menu View. The Home Button is located in this view.
import SwiftUI
import Combine
import Foundation
struct MenuView: View {
#EnvironmentObject var userSettings: UserSettings
#State var showMenu = false
#State var Homevariable = false
var body: some View {
VStack(alignment: .leading) {
Button(action: {
UserDefaults.standard.set(false, forKey: "status")
}) {
(Text(Image(systemName: "rectangle.righthalf.inset.fill.arrow.right")) + (Text("Home")))
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
.edgesIgnoringSafeArea(.all)
}
}
struct MenuView_Previews: PreviewProvider {
static var previews: some View {
MenuView()
.environmentObject(UserSettings())
}
}
//This is the another view. I want the side Menu to appear on this as well, so when I press the Home button it takes me to the Main View("This is the Main View")
import SwiftUI
struct Calculation: View {
var body: some View {
Text("Hello, World!")
}
}
struct Calculation_Previews: PreviewProvider {
static var previews: some View {
Calculation()
}
}
Here you go. You are basically rebuilding a navigation logic, so in MainView you have to switch between the screens and put the side menu over it:
(PS: you can do without GeometryReader)
struct ContentView: View {
#State private var showMenu = false
#State private var selected: SelectedScreen = .home
var body: some View {
NavigationView {
ZStack {
// show selected screen
switch selected {
case .home:
MainView()
.disabled(self.showMenu ? true : false)
case .screen1:
OtherView(screen: 1)
case .screen2:
OtherView(screen: 2)
}
// put menu over it
if self.showMenu {
MenuView(showMenu: $showMenu, selected: $selected)
.transition(.move(edge: .leading))
}
}
.navigationBarTitle("Pay Data", displayMode: .inline)
// .navigationBarItems is deprecated, use .toolbar
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
withAnimation {
self.showMenu.toggle()
}
} label: {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}
}
}
}
}
}
enum SelectedScreen {
case home
case screen1
case screen2
}
struct MenuView: View {
#Binding var showMenu: Bool
#Binding var selected: SelectedScreen
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 24) {
Button {
selected = .home
showMenu = false
} label: {
Label("Home", systemImage: "rectangle.righthalf.inset.fill.arrow.right")
}
Button {
selected = .screen1
showMenu = false
} label: {
Label("Screen 1", systemImage: "1.circle")
}
Button {
selected = .screen2
showMenu = false
} label: {
Label("Screen 2", systemImage: "2.circle")
}
}
.padding()
.frame(maxHeight: .infinity)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
Spacer()
}
}
}
struct MainView: View {
var body: some View {
Text("This is Main View")
.font(.largeTitle)
}
}
struct OtherView: View {
let screen: Int
var body: some View {
Text("Other View: Screen \(screen)")
.font(.largeTitle)
}
}

SwiftUI Textfield not actually making updates to data

I am trying to make an application where the user can see a list of foods and recipes and make changes etc. In the DetailView for each food, there is an EditView where the values can be changed. I am trying to use a double binding on the value by using #State to define the value and $food.name for example to make the changes happen.
When I click 'Done' and exit this view back to the DetailView, the changes are not made at all, leaving me confused?
Any help on this problem would be greatly appreciated, thank you :)
My EditView.swift:
import SwiftUI
struct EditView: View {
#State var food: Food
var body: some View {
List {
Section(header: Text("Name")) {
TextField("Name", text: $food.name)
}
Section(header: Text("Image")) {
TextField("Image", text: $food.image)
}
Section(header: Text("Desc")) {
TextField("Desc", text: $food.desc)
}
Section(header: Text("Story")) {
TextField("Story", text: $food.story)
}
}.listStyle(InsetGroupedListStyle())
}
}
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(food: cottonCandy)
}
}
My FoodDetail.swift:
import SwiftUI
struct FoodDetail: View {
#State var food: Food
#State private var isPresented = false
var body: some View {
VStack {
Image(food.image)
.resizable()
.frame(width: 300.0,height:300.0)
.aspectRatio(contentMode: .fill)
.shadow(radius: 6)
.padding(.bottom)
ScrollView {
VStack(alignment: .leading) {
Text(food.name)
.font(.largeTitle)
.fontWeight(.bold)
.padding(.leading)
.multilineTextAlignment(.leading)
Text(food.desc)
.italic()
.fontWeight(.ultraLight)
.padding(.horizontal)
.multilineTextAlignment(.leading)
Text(food.story)
.padding(.horizontal)
.padding(.top)
Text("Ingredients")
.bold()
.padding(.horizontal)
.padding(.vertical)
ForEach(food.ingredients, id: \.self) { ingredient in
Text(ingredient)
Divider()
}.padding(.horizontal)
Text("Recipe")
.bold()
.padding(.horizontal)
.padding(.vertical)
ForEach(food.recipe, id: \.self) { step in
Text(step)
Divider()
}.padding(.horizontal)
}.frame(maxWidth: .infinity)
}.frame(minWidth: 0,
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .center
)
}
.navigationBarItems(trailing: Button("Edit") {
isPresented = true
})
.fullScreenCover(isPresented: $isPresented) {
NavigationView {
EditView(food: food)
.navigationTitle(food.name)
.navigationBarItems(leading: Button("Cancel") {
isPresented = false
}, trailing: Button("Done") {
isPresented = false
})
}
}
}
}
struct FoodDetail_Previews: PreviewProvider {
static var previews: some View {
Group {
FoodDetail(food: cottonCandy)
}
}
}
Inside EditView, you want to have a Binding. Replace
#State var food: Food
with
#Binding var food: Food
... then, you'll need to pass it in:
.fullScreenCover(isPresented: $isPresented) {
NavigationView {
EditView(food: $food) /// make sure to have dollar sign
.navigationTitle(food.name)
.navigationBarItems(leading: Button("Cancel") {
isPresented = false
}, trailing: Button("Done") {
isPresented = false
})
}
}

How do I handle selection in NavigationView on macOS correctly?

I want to build a trivial macOS application with a sidebar and some contents according to the selection in the sidebar.
I have a MainView which contains a NavigationView with a SidebarListStyle. It contains a List with some NavigationLinks. These have a binding for a selection.
I would expect the following things to work:
When I start my application the value of the selection is ignored. Neither is there a highlight for the item in the sidebar nor a content in the detail pane.
When I manually select an item in the sidebar it should be possible to navigate via up/down arrow keys between the items. This does not work as the selection / highlight disappears.
When I update the value of the selection-binding it should highlight the item in the list which doesn't happen.
Here is my example implementation:
enum DetailContent: Int, CaseIterable {
case first, second, third
}
extension DetailContent: Identifiable {
var id: Int { rawValue }
}
class NavigationRouter: ObservableObject {
#Published var selection: DetailContent?
}
struct DetailView: View {
#State var content: DetailContent
#EnvironmentObject var navigationRouter: NavigationRouter
var body: some View {
VStack {
Text("\(content.rawValue)")
Button(action: { self.navigationRouter.selection = DetailContent.allCases.randomElement()!}) {
Text("Take me anywhere")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct MainView: View {
#ObservedObject var navigationRouter = NavigationRouter()
#State var detailContent: DetailContent? = .first
var body: some View {
NavigationView {
List {
Section(header: Text("Section")) {
ForEach(DetailContent.allCases) { item in
NavigationLink(
destination: DetailView(content: item),
tag: item,
selection: self.$detailContent,
label: { Text("\(item.rawValue)") }
)
}
}
}
.frame(minWidth: 250, maxWidth: 350)
}
.environmentObject(navigationRouter)
.listStyle(SidebarListStyle())
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(navigationRouter.$selection) { output in
self.detailContent = output
}
}
}
The EnvironmentObject is used to propagate the change from inside the DetailView. If there's a better solution I'm very happy to hear about it.
So the question remains:
What am I doing wrong that this happens?
I had some hope that with Xcode 11.5 Beta 1 this would go away but that's not the case.
After finding the tutorial from Apple it became clear that you don't use NavigiationLink on macOS. Instead you bind the list and add two views to NavigationView.
With these updates to MainView and DetailView my example works perfectly:
struct DetailView: View {
#Binding var content: DetailContent?
#EnvironmentObject var navigationRouter: NavigationRouter
var body: some View {
VStack {
Text("\(content?.rawValue ?? -1)")
Button(action: { self.navigationRouter.selection = DetailContent.allCases.randomElement()!}) {
Text("Take me anywhere")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct MainView: View {
#ObservedObject var navigationRouter = NavigationRouter()
#State var detailContent: DetailContent?
var body: some View {
NavigationView {
List(selection: $detailContent) {
Section(header: Text("Section")) {
ForEach(DetailContent.allCases) { item in
Text("\(item.rawValue)")
.tag(item)
}
}
}
.frame(minWidth: 250, maxWidth: 350)
.listStyle(SidebarListStyle())
DetailView(content: $detailContent)
}
.environmentObject(navigationRouter)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(navigationRouter.$selection) { output in
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
self.detailContent = output
}
}
}
}

SwiftUI: Can't get the transition of a DetailView to a ZStack in the MainView to work

I can't find the answer to this anywhere, hopefully one of you can help me out.
I have a MainView with some content. And with the press of a button I want to open a DetailView. I am using a ZStack to layer the DetailView on the top, filling the screen.
But with the following code I can't get it to work. The DetailView does not have a transition when it inserts and it stops at removal. I have tried with and without setting the zIndex manually, and a custom assymetricalTransition. Couldn't get that to work. Any solutions?
//MainView
#State var showDetail: Bool = false
var body: some View {
ZStack {
VStack {
Text("Hello MainWorld")
Button(action: {
withAnimation(.spring()) {
self.showDetail.toggle()
}
}) {
Text("Show detail")
}
}
if showDetail {
ContentDetail(showDetail: $showDetail)
.transition(.move(edge: .bottom))
}
}
.edgesIgnoringSafeArea(.all)
}
And here is the DetailView:
//DetailView
#Binding var showDetail: Bool
var body: some View {
VStack (spacing: 25) {
Text("Hello, DetailWorld!")
Button(action: { withAnimation(.spring()) {
self.showDetail.toggle()
}
}) {
Text("Close")
}
.padding(.bottom, 50)
}
.frame(width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
.background(Color.yellow)
.edgesIgnoringSafeArea(.all)
}
The result of this code is this:
I'm running Xcode 11.4.1 so implicit animations doesn't seem to work either. Really stuck here, hope one of you can help me out! Thanks :)
Here is a solution. Tested with Xcode 11.4 / iOS 13.4.
struct MainView: View {
#State var showDetail: Bool = false
var body: some View {
ZStack {
Color.clear // extend ZStack to all area
VStack {
Text("Hello MainWorld")
Button(action: {
self.showDetail.toggle()
}) {
Text("Show detail")
}
}
if showDetail {
DetailView(showDetail: $showDetail)
.transition(AnyTransition.move(edge: .bottom))
}
}
.animation(Animation.spring()) // one animation to transitions
.edgesIgnoringSafeArea(.all)
}
}
struct DetailView: View {
#Binding var showDetail: Bool
var body: some View {
VStack (spacing: 25) {
Text("Hello, DetailWorld!")
Button(action: {
self.showDetail.toggle()
}) {
Text("Close")
}
.padding(.bottom, 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity) // fill in container
.background(Color.yellow)
}
}

Updating #Published variable of an ObservableObject inside child view

I have a published variable isLoggedIn inside a ObservableObject class as follows:
import Combine
class UserAuth: ObservableObject{
#Published var isLoggedIn: Bool = false
}
I want to update this variable to true in a particular view (LoginView). This variable determines what view I show the user depending if the user has logged in or not:
struct ContentView: View {
#ObservedObject var userAuth = UserAuth()
var body: some View {
Group{
if(userAuth.isLoggedIn){
MainView()
}else{
AccountView()
}
}
}
}
Because userAuth.isLoggedIn is false (I haven't logged in) AccountView is displayed.
AccountView:
struct AccountView: View {
#State private var toggleSheet = false
var body: some View {
VStack {
Spacer()
Button(action: {
self.toggleSheet.toggle()
}){
Text("Toggle Modal")
.padding()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10)
}
.sheet(isPresented: self.$toggleSheet){
LoginView()
}
Spacer()
}
}
}
Whenever the user presses the button the LoginView Modal is shown:
struct LoginView: View {
var body: some View {
VStack{
Button(action: {
return self.login()
}){
Text("Log in")
.padding()
.foregroundColor(Color.white)
.background(Color.green)
.cornerRadius(10)
}
}
}
func login(){
// update UserAuth().isLoggedIn to TRUE
}
}
In the LoginView there is a button, the logic I want is for the user to press the button, login() gets called and inside that function userAuth.isLoggedIn is set to true. What would be the best way to implement this ?
I've tried to directly change the value and I get an error along the lines of:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive
Try embedding your code in DispatchQueue.main.async like this:
func login(){
DispatchQueue.main.async {
//update UserAuth().isLoggedIn to TRUE
}
}
One possibility would be to insert the UserAuth object from the ContentView into the subviews as an EnvironmentObject.
struct ContentView: View {
#ObservedObject var userAuth = UserAuth()
var body: some View {
Group {
if userAuth.isLoggedIn {
MainView()
} else {
AccountView()
.environmentObject(userAuth)
}
}
}
}
Now userAuth is accessible in AccountView and all its subviews (e.g. LoginView):
struct LoginView: View {
// Access to the environment object
#EnvironmentObject private var userAuth: UserAuth
var body: some View {
VStack {
Button(action: {
return self.login()
}) {
Text("Log in")
.padding()
.foregroundColor(Color.white)
.background(Color.green)
.cornerRadius(10)
}
}
}
func login() {
// Update the value on the main thread
DispatchQueue.main.async {
self.userAuth.isLoggedIn = true
}
}
}
It may be necessary to insert the EnvironmentObject into the LoginView manually. You can do this by accessing it in the AccountView and inserting it in the LoginView:
struct AccountView: View {
#State private var toggleSheet = false
// Access the value, that was inserted in `ContentView`
#EnvironmentObject private var userAuth: UserAuth
var body: some View {
VStack {
Spacer()
Button(action: {
self.toggleSheet.toggle()
}){
Text("Toggle Modal")
.padding()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10)
}
.sheet(isPresented: self.$toggleSheet){
LoginView()
// Insert UserAuth object again
.environmentObject(self.userAuth)
}
Spacer()
}
}
}
Further Reading
WWDC 2019 Video | Hacking with Swift Example