SWIFTUI : navigate in view and go back - swiftui

I have a question about swift. How I can navigate into views ?
Now, I have my ContentView that display a launchScreen, or Login, or HomeView in a conditional (if).
var body: some View {
VStack {
if (sessionStore.session != nil) {
UserProfileView(userProfile: self.profile ?? UserProfile(uid: "", firstName: "", lastName: "", birth: "", email: "", phone: ""))
} else if show || UserDefaults.standard.bool(forKey: initialLaunchKey){
AuthentificationView().transition(.move(edge: .bottom))
} else {
PageViewContainer( viewControllers: Page.getAll.map({ UIHostingController(rootView: PageView(page: $0) ) }), presentSignupView: { withAnimation { self.show = true }; UserDefaults.standard.set(true, forKey: self.initialLaunchKey) }).transition(.scale)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.backgroundColor)
.onTapGesture { UIApplication.shared.endEditing() }
}
But if I am in the LoginView, even if my condition in contentView for HomeView become true, I'm not going to HomeView...
I navigate into view by a var in observable object (page =1 then page =2...) I think is not the better way...
struct AuthentificationView: View {
#EnvironmentObject var userSignup: UserSignup
var body: some View {
VStack {
if (userSignup.page >= 1) {
SignupView()
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
} else {
LoginView()
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
}
}
}
}
Thanks

I cannot test your code (due to absent custom things), so just by experience try the following
use explicit condition for every view with transition
make transitions animatable
make container holding transitions animatable
Like below (scratchy)
var body: some View {
VStack {
if userSignup.page >= 1 {
SignupView()
.transition(AnyTransition.asymmetric(insertion: .move(edge: .trailing),
removal: .move(edge: .leading)).animation(.default))
}
if userSignup.page == 0 {
LoginView()
.transition(AnyTransition.asymmetric(insertion: .move(edge: .leading),
removal: .move(edge: .trailing)).animation(.default))
}
}.animation(.default)
}

Related

Slider transition animation bug

I have this strange animation bug I've not been able to figure out. When transitioning on a view that contains a slider, the slider appears to animate independently of the rest of the view.
For reference, here is the code that animates the two views:
#main
struct TransitionAnimationBug: App {
#State var displaySettings = false
var body: some Scene {
WindowGroup {
Group {
if displaySettings {
SliderView(displaySettings: $displaySettings)
.transition(AnyTransition.asymmetric(
insertion: .move(edge: .trailing),
removal: .move(edge: .leading)
))
} else {
MainView(displaySettings: $displaySettings)
.transition(AnyTransition.asymmetric(
insertion: .move(edge: .leading),
removal: .move(edge: .trailing)
))
}
}
.animation(.easeInOut, value: displaySettings) // Shows bug
// .animation(.spring(), value: displaySettings) // Works
}
}
}
And here is the code from the settings screen that displays the slider:
struct SliderView: View {
var displaySettings: Binding<Bool>
#State var sliderValue: Double = 4.0
init(displaySettings: Binding<Bool>) {
self.displaySettings = displaySettings
}
var body: some View {
VStack(spacing: 30) {
Text("Hello, world!")
Slider(value: $sliderValue, in: 0 ... 4, step: 1)
.border(.red)
.padding(20)
Button("Close") { displaySettings.wrappedValue.toggle() }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.black.opacity(0.2))
}
}
And for completeness the main view:
struct MainView: View {
var displaySettings: Binding<Bool>
init(displaySettings: Binding<Bool>) {
self.displaySettings = displaySettings
}
var body: some View {
VStack(spacing: 30) {
Text("Hello, world!")
.padding()
Button("Show transition bug") {
displaySettings.wrappedValue.toggle()
}
Text("The slider will animate incorrectly")
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.white)
}
}
As you can see this is a minimal app I built to show the bug (which I've submitted to Apple via their Feedback assistant), but I was wondering if anyone here as encountered it and found a solution.
Through experimenting I found that using a .spring() animation instead of .slideInOut correctly animates the slider, but any other animation fails.
Any thoughts?
This is effect of changing root view of window. The fix is to move the content of WindowGroup with all related states into separated view (to make root of window persistent), so it looks like
WindowGroup {
ContentView() // << everything else move here !!
}
Tested with Xcode 13.3 / iOS 15.4
Thanks Asperi, that worked. Do you know why? On first glance there's no obvious reason why it would work other than it being something to do with the fact that the body returns a Scene as opposed to a View.
The changed code looks like this:
#main
struct TransitionAnimationBug: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var displaySettings = false
var body: some View {
Group {
if displaySettings {
SliderView(displaySettings: $displaySettings)
.transition(AnyTransition.asymmetric(
insertion: .move(edge: .trailing),
removal: .move(edge: .leading)
))
} else {
MainView(displaySettings: $displaySettings)
.transition(AnyTransition.asymmetric(
insertion: .move(edge: .leading),
removal: .move(edge: .trailing)
))
}
}
.animation(.easeInOut, value: displaySettings)
}
}

SwiftUI: How to pass an argument from one view to the next with dynamically generated buttons?

Problem:
I am unable to force my alpha, beta, or gamma buttons to turn ON when an input parameter is passed from Landing.swift.
I do not understand why when onAppear fires in the stack, the output becomes:
gamma is the title
beta is the title
alpha is the title
gamma is the title
beta is the title
alpha is the title
Confused -> Why is this outputting 2x when the ForEach loop has only 3 elements inside?
Background:
I am trying to pass a parameter from one view (Landing.swift) to another (ContentView.swift) and then based on that parameter force the correct button (in ContentView) to trigger an ON state so it's selected. I have logic shown below in ButtonOnOff.swift that keeps track of what's selected and not.
For instance, there are 3 buttons in ContentView (alpha, beta, and gamma) and based on the selected input button choice from Landing, the respective alpha, beta, or gamma button (in ContentView) should turn ON.
I am dynamically generating these 3 buttons in ContentView and want the flexibility to extend to possibly 10 or more in the future. Hence why I'm using the ForEach in ContentView. I need some help please understanding if I'm incorrectly using EnvironmentObject/ObservedObject or something else.
Maintaining the ON/OFF logic works correctly with the code. That is, if you manually press alpha, it'll turn ON but the other two will turn OFF and so forth.
Thanks for your help in advance! :)
Testing.swift
import SwiftUI
#main
struct Testing: App {
#StateObject var buttonsEnvironmentObject = ButtonOnOff()
var body: some Scene {
WindowGroup {
Landing().environmentObject(buttonsEnvironmentObject)
}
}
}
Landing.swift
import SwiftUI
struct Landing: View {
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(landingChoice:tag ?? ""), tag: tag ?? "", selection: $tag) {
EmptyView()
}
Button(action: {
self.tag = "alpha"
}) {
HStack {
Text("alpha")
}
}
Button(action: {
self.tag = "beta"
}) {
HStack {
Text("beta")
}
}
Button(action: {
self.tag = "gamma"
}) {
HStack {
Text("gamma")
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var btnName:String
#EnvironmentObject var buttonEnvObj:ButtonOnOff
init(landingChoice:String){
self.btnName = landingChoice
print("\(self.btnName) is the input string")
}
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach(0..<buttonEnvObj.buttonNames.count) { index in
BubbleButton(label: "\(buttonEnvObj.buttonNames[index])")
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(buttonEnvObj.buttonNames[index]) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var label: String
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: self.label)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(label)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(buttonBrandButtons.buttonBrand[self.label]! ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(buttonBrandButtons.buttonBrand[self.label]! ? Color.blue : .clear)
.cornerRadius(15)
.overlay(buttonBrandButtons.buttonBrand[self.label]! ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
ButtonOnOff.swift
import Foundation
class ButtonOnOff:ObservableObject{
var buttonNames = ["alpha","beta","gamma"]
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}
For a short answer just add
.onAppear(){
buttonEnvObj.changeState(buttonName: self.btnName)
}
to ContentView that will highlight the button that was selected.
As for a solution that can be expanded at will. I would suggest a single source of truth for everything and a little simplifying.
struct Landing: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(), tag: tag ?? "", selection: $tag) {
EmptyView()
}
//Put your buttons here
HStack{
//Use the keys of the dictionary to create the buttons
ForEach(buttonEnvObj.buttonBrand.keys.sorted(by: <), id: \.self){ key in
//Have the button set the value when pressed
Button(action: {
self.tag = key
buttonEnvObj.changeState(buttonName: key)
}) {
Text(key)
}
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct ContentView: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
//Change this to use the dictionary
ForEach(buttonEnvObj.buttonBrand.sorted(by: {$0.key < $1.key }), id:\.key) { key, value in
BubbleButton(key: key, value: value)
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(value) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var key: String
var value: Bool
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: key)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(key)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(value ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(value ? Color.blue : .clear)
.cornerRadius(15)
.overlay(value ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
class ButtonOnOff:ObservableObject{
//Get rid of this so you can keep the single source
//var buttonNames = ["alpha","beta","gamma"]
//When you want to add buttons just add them here it will all adjust
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}

Binding two ForEach loop to update each item cell

This is my second post and I need your help as much as possible. I am creating a favorite button on my parent view and detail view. I need both buttons to work correspondent to each other. When I marked favorite on the ForEach loop of my parent view, I want to show the item is favorited in my detail view. Also, I can unfavorite or favorite from my detail view vice vasa. It is really hard for me to figure out how to bind those two ForEach loops. Below I provide an example of my codes. If you want to test with my full code, you can access it here: Making favorite button from several layers and binding two list using EnvironmentObject
struct Data: Identifiable {
let id = UUID()
let number: Int
var name1: String
let name2: String
}
public struct DataList {
static var dot = [
Data(number: 1,
name1: "Pasian Phatna",
name2: "Praise God, from whom All Blessings Flow"),
Data(number: 2,
name1: "Itna Kumpi, Ka Tuu-Cing Pa",
name2: "The King of Love My Shephaerd Is (Dominus Regit Me)"),
Data(number: 3,
name1: "Kumpipa Bia Un",
name2: "O Worship the King"),
Data(number: 4,
name1: "Pa Tung Min Than'na Om Hen",
name2: "Gloria Patri (1st Tune)"),
Data(number: 5,
name1: "Pa Tung Min Than'na Om Hen",
name2: "Gloria Patri (2nd Tune)")
]
}
struct ParentView: View {
#State var datas: [Data] = DataList.dot
var body: some View {
NavigationView {
ScrollView (.vertical, showsIndicators: false) {
LazyVStack(spacing: 5) {
ForEach (datas, id: \.id) { data in
MainData(data: data)
Divider()
.padding(.all)
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct MainData: View {
#State var data: Data
#State var selectedFavoriteSong: Bool = false
var body: some View {
HStack {
Button(action: {
self.selectedFavoriteSong.toggle()
}, label: {
if selectedFavoriteSong {
Image(systemName: "suit.heart.fill")
.foregroundColor(.red)
.padding(.horizontal)
} else {
Image(systemName: "suit.heart")
.padding(.horizontal)
}
})
Spacer()
Text("\(data.number)")
Spacer()
}
.padding(.top)
VStack {
Text(data.name1)
.font(.title2.smallCaps())
.fontWeight(.bold)
.foregroundColor(.primary)
Text(data.name2)
.font(.title3)
.fontWeight(.medium)
.foregroundColor(.secondary)
.italic()
}
.padding(.horizontal)
.multilineTextAlignment(.center)
}
}
Please consider, the Search() below will pop up when I tapped the search icon (which is not presented here). My point is the Search() is not directly connect to the ParentView() but the DetailView() is embedded in the Search().
struct Search: View {
#State var datas: [Data] = DataList.dot
var body: some View {
NavigationView {
ScrollView (.vertical, showsIndicators: false) {
LazyVStack(alignment: .leading, spacing: 10) {
ForEach (datas, id: \.id) { data in
NavigationLink(
destination: DetailView(data: data),
label: {
Text("Search")
})
}
}.padding(.horizontal)
}
}
}
}
struct DetailView: View {
#State var data: Data
#State var selectedFavoriteSong: Bool = false
var body: some View {
HStack {
Button(action: {
self.selectedFavoriteSong.toggle()
}, label: {
if selectedFavoriteSong {
Image(systemName: "suit.heart.fill")
.foregroundColor(.red)
.padding(.horizontal)
} else {
Image(systemName: "suit.heart")
.padding(.horizontal)
}
})
Spacer()
Text("\(data.name1)")
Spacer()
}
.padding(.top)
VStack {
Text(data.name2)
.font(.title2.smallCaps())
.fontWeight(.bold)
.foregroundColor(.primary)
}
.padding(.horizontal)
.multilineTextAlignment(.center)
Spacer()
}
}
So, I want to connect the parent view and the detail view with some kind of binding property. But there is impossible to connect these two. I can store
#State var selectedFavoriteSong: Bool = false
inside the EnvironmentObject. But when I click favorite, all the items inside the ForEach loop are selected. Please help me on this issue. If you need a full code, the above link will direct to my first post. Thank you.
I'd suggest storing all of your data in an ObservableObject that is owned by the parent view and then can get passed into subviews (either explicitly or via an EnvironmentObject):
class DataSource : ObservableObject {
#Published var data : [Data] = DataList.dot
#Published var favoritedItems: Set<UUID> = []
func favoriteBinding(forID id: UUID) -> Binding<Bool> {
.init {
self.favoritedItems.contains(id)
} set: { newValue in
if newValue {
self.favoritedItems.insert(id)
} else {
self.favoritedItems.remove(id)
}
}
}
}
For example:
struct ParentView : View {
#StateObject var dataSource = DataSource()
var body: some View {
VStack {
Search(dataSource: dataSource)
}
}
}
Note that the data source stores a list of IDs that have been favorited. It uses a custom binding that can pass the boolean value down to a detail view:
struct Search: View {
#ObservedObject var dataSource : DataSource
var body: some View {
NavigationView {
ScrollView (.vertical, showsIndicators: false) {
LazyVStack(alignment: .leading, spacing: 10) {
ForEach (dataSource.data, id: \.id) { data in
NavigationLink(
destination: DetailView(data: data,
selectedFavoriteSong: dataSource.favoriteBinding(forID: data.id)),
label: {
Text(data.name1)
})
}
}.padding(.horizontal)
}
}
}
}
struct DetailView: View {
var data : Data
#Binding var selectedFavoriteSong : Bool
var body: some View {
HStack {
Button(action: {
self.selectedFavoriteSong.toggle()
}, label: {
if self.selectedFavoriteSong {
Image(systemName: "suit.heart.fill")
.foregroundColor(.red)
.padding(.horizontal)
} else {
Image(systemName: "suit.heart")
.padding(.horizontal)
}
})
Spacer()
Text("\(data.name1)")
Spacer()
}
.padding(.top)
VStack {
Text(data.name2 ?? "")
.font(.title2.smallCaps())
.fontWeight(.bold)
.foregroundColor(.primary)
}
.padding(.horizontal)
.multilineTextAlignment(.center)
Spacer()
}
}

SwiftUI, Navigation view and animated transitions in destination. VStack works but List does not

The RoundedRectangle "animates out" but "snaps in" if I use List.
Is it a SwiftUI bug or me?
Also, If the third navigation link is commented out then the button in the rectangles view pops the app back to the Navigation view. Next time I go to the rectangles it has changed the color.
This happens only with VStack, List is unaffected.
This makes no sense at all.
import SwiftUI
struct ContentView: View {
#State var fff: Bool = false
var body: some View {
NavigationView {
VStack { // <---- This works if I have three navigation links, but not if I have only two.
// List { // <------ This does not work.
NavigationLink(
destination:
ZStack {
if fff {
RoundedRectangle(cornerRadius: 100) .foregroundColor(.blue)
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top)))
.animation(.easeIn)
}
else {
RoundedRectangle(cornerRadius: 100) .foregroundColor(.purple)
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top)))
.animation(.easeIn)
}
Button(action: {
fff.toggle()
}, label: {
Text("Button")
})
}
,
label: {
Text("To rectangles")
}
)
NavigationLink(
destination: Text("Settings"),
label: {
HStack {
Image(systemName: "wrench")
Text("Settings")
}
}
)
// NavigationLink( <--- If this link is commented out then the button in the rectangles view pops the app back to the Navigation view (only when using VStack).
// destination: Text("Store"),
// label: {
// HStack {
// Image(systemName: "cart")
// Text("Store")
// }
// }
// )
}
}
}
}
Moving the destination to a different struct fixes the issue.
struct RectView: View {
#State var fff: Bool = false
var body: some View {
ZStack {
if fff {
RoundedRectangle(cornerRadius: 100) .foregroundColor(.blue)
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top)))
.animation(.easeIn)
} else {
RoundedRectangle(cornerRadius: 100) .foregroundColor(.purple)
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top)))
.animation(.easeIn)
}
Button(action: {
fff.toggle()
}, label: {
Text("Button")
})
}
}
}

SwiftUI state variable asymmetric transition

Is it possible to change an asymmetric transition while on screen. For example, the text has a asymmetric transition of
.transition(.asymmetric(insertion: .move(edge: .trailing) removal: .move(edge: .leading)))
While the text is on the screen the transition needs to be changed to this
.transition(.asymmetric(insertion: .move(edge: .trailing) removal: .opacity))
I have created a test program which uses a #State variable that changes the transitions. The issue is when trying to change the removal transition after the view has been insertion
To simulate the problem with the code below simply press "animate text" then "change animation" then "animate text". You will see the change animation has no effect until you press "animate text" a third time (when the text is off the screen).
import SwiftUI
// Xcode 11.3
struct ContentView: View {
#State var buttonClick: Bool = false
#State var showText: Bool = false
var body: some View {
VStack(spacing: 10){
if showText == true{
Text("hello")
.transition(.asymmetric(insertion: buttonClick ? .move(edge: .trailing) : .opacity, removal: buttonClick ? .move(edge: .leading) : .opacity))
}
Button (action: { withAnimation { self.buttonClick.toggle() } }) {
Text("change transition")
}
Button (action: { withAnimation { self.showText.toggle() }}) {
Text("Animate text")
}
}
}
}