There's a view in my app called ContinueView that slides out of sight when a button is clicked and it works like so:
struct ContentView: View {
#Environment(\.accessibilityReduceMotion) var isReducedMotion
#State private var hasRequestedNotifications = UserDefaults.standard.bool(forKey: "hasRequestedNotifications")
#State private var isShowingContinueView = !UserDefaults.standard.bool(forKey: "isLoggedIn")
var body: some View {
.........
if (isShowingContinueView) {
ContinueView(isShowingContinueView: $isShowingContinueView).transition(.asymmetric(insertion: AnyTransition.move(edge: .trailing).combined(with: .opacity), removal: AnyTransition.move(edge: .leading).combined(with: .opacity)))
} else if (!hasRequestedNotifications) {
AllowNotificationsView(hasRequestedNotifications: $hasRequestedNotifications).transition(.asymmetric(insertion: AnyTransition.move(edge: .trailing).combined(with: .opacity), removal: AnyTransition.move(edge: .leading).combined(with: .opacity)))
}
........
}
}
ContinueView:
....
Button("Continue") {
withAnimation {
self.isShowingContinueView.wrappedValue = false
}
}
.....
This works perfectly, it shows and hides the view with a smooth sliding transition. But for users with reduced motion, I wanted to support it too so that instead of sliding, it just shows and hides with no animation at all.
I've got an enviroment variable at the top of my contentView called: isReducedMotion, with this I want to toggle motion on and off dependent on what the variable is set to.
I've tried putting it like this:
ContinueView(isShowingContinueView: $isShowingContinueView).transition(isReducedMotion ? nil : .asymmetric(insertion: AnyTransition.move(edge: .trailing).combined(with: .opacity), removal: AnyTransition.move(edge: .leading).combined(with: .opacity)))
But it shows this error: 'nil' cannot be used in context expecting type 'AnyTransition'
So i'm a bit stuck with it and not sure how to get it to work. Does anyone know how to do this? Any help would be really appreciated.
P.S: The code i've put here is modified so its a bit simpler, but yes, the state variables work fine, and the isShowingContinueView variable does get set to true/false elsewhere in the code :)
Also if you have any questions feel free to hit me up since I know my question might be a bit confusing :D
You can try using .identity instead of nil for the transition, or you can pass nil to withAnimation to specify no animation:
Button("Continue") {
withAnimation(isReducedMotion ? nil : .default) {
self.isShowingContinueView.wrappedValue = false
}
}
Related
I'm stuck on something which should be trivial, using SwiftUI. I am pulling some data back from my API, and simply want to show each item on my view with ForEach.
I have the following function:
#State var workoutVideoList: GetWorkoutVideoListResponse!
func GetWorkoutVideoList() {
loadingWorkoutVideoList = true
PtPodsApiService.GetWorkoutList(firebaseToken: firebaseAuthToken) { (workoutListResult) -> () in
DispatchQueue.main.async() {
workoutVideoList = workoutListResult
loadingWorkoutVideoList = false
}
}
}
I then want to show another view and pass this object into it:
if workoutVideoList != nil {
BookAppointmentView(workoutVideoList: workoutVideoList)
}
else {
Text("Nope")
}
For whatever reason, my main thread sees workoutVideoList as nil, even though it's been assigned. I thought using the dispatcher to assign it would solve the problem, but no joy!
Any idea what I'm doing wrong here?
Thanks
Give #State a default value #State var workoutVideoList: GetWorkoutVideoListResponse = false and use onAppear to call GetWorkoutVideoList() but ideally you will eventually get this working in a ViewModel.
I would like to slide in a view over the bottom, to display a small note, and then slide off after a few seconds..
slide in is working fine, but when it slides out, it glitches:
if let err = error {
ErrorView(err)
.animation(.spring())
.transition(.move(edge: .bottom))
.onAppear(perform: errorAppeared)
}
When there is an error, it transitions just fine, but when there isn't, instead of sliding down or inversing, it just pops off the view
I am calling this function to clear the view:
private func clearError() {
withAnimation {
self.error = nil
}
}
none of the existing questions helped, I tried to do things in the 'onReceive' function and about every other answer I could find, most say it should just work, or to use animation, or transition, I believe I've tried most combinations and at this point am quite stuck
I assume the animation is just in wrong place - put it for container holding condition and link it to dependent state variable, like below
VStack { // << can be as-is, only for condition
if let err = error {
ErrorView(err)
.transition(.move(edge: .bottom))
.onAppear(perform: errorAppeared)
}
}
.animation(.spring(), value: error) // << here !!
and then just activate in regular way
private func clearError() {
self.error = nil
}
I have a view which displays a list of posts. I have implemented infinite scrolling, and it is functioning properly. however, there is one small problem I am running into, and attempts to solve it have me going round in circles.
Main view
struct PostsHomeView: View {
#EnvironmentObject var viewModel: ViewModel
#State var dataInitiallyFetched = false
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 0) {
if self.viewModel.posts.count > 0 {
PostsListView(posts: self.viewModel.posts,
isLoading: self.viewModel.canFetchMorePosts,
onScrolledAtBottom: self.viewModel.fetchMorePosts
)
} else {
VStack {
Text("You have no posts!")
}
}
}
.onAppear() {
if !self.dataInitiallyFetched {
self.viewModel.fetchMostRecentPosts()
self.dataInitiallyFetched = true
}
}
.navigationBarTitle("Posts", displayMode: .inline)
}
}
}
List view
struct PostsListView: View {
#EnvironmentObject var viewModel: ViewModel
let posts: [Post]
let isLoading: Bool
let onScrolledAtBottom: () -> Void
var body: some View {
List {
postsList
if isLoading {
loadingIndicator
}
}
}
private var postsList: some View {
ForEach(posts, id: \.self) { post in
PostsCellView(post: post)
.onAppear {
if self.posts.last == post {
self.onScrolledAtBottom()
}
}
}
.id(UUID())
}
}
Problem
Upon tapping one of the posts in the list, I am taken to a detail view. When I tap the nav bar's back button in order go back to the posts list, the whole view is reloaded and my post fetch methods are fired again.
In order to stop the fetch method that fetches most recent posts from firing, I have added a flag that I set to true after the initial load. This stops the fetch method that grabs the initial set of posts from firing when I go back and forth between the details view and posts home screen.
I have tried various things to stop the fetchMorePosts function from firing, but I keep going in circles. I added a guard statement to the top of the fetchMorePosts function in my view model. It checks to see if string is equal to "homeview", if not, then the fetch is not done. I set this string to "detailview" whenever the detail view is visited, then I reset it back to "homeview" in the guard statement.
guard self.lastView == "homeview" else {
self.lastView = "homeview"
return
}
This works to an extent, but I keep finding scenarios where it doesn't work as expected. There must be a straight-forward way to tell SwiftUI not to reload a view. The problem is the method sits in the onAppear closure which is vital for the infinite scrolling to work. I'm not using iOS 14 yet, so I can't use #StateObject.
Is there a way to tell SwiftUI not to fire onAppear everytime I return from a detail view?
Thanks in advance
The culprit was .id(UUID()). I removed it from my list and everything worked again.
Thanks Asperi. Your help is much appreciated.
I have a view that is being called using .sheet in SwiftUI. However, when this sheet is presented, I'm getting the debug "Tapped" print in console followed by this error:
Warning: Attempt to present <_TtGC7SwiftUIP13$7fff2c9bdf5c22SheetHostingControllerVS_7AnyView_: 0x7f8f1d7400f0> on <_TtGC7SwiftUI19UIHostingControllerVVS_22_VariadicView_Children7Element_: 0x7f8f17e0dae0> which is already presenting (null)
I'm not exactly sure what is causing this error, but from what I understand it's due to the view getting called twice. I'm not sure how the view is being called twice, or even if it is, which is why I'm asking here. Below is the main view that actually houses the NavigationView in which my List is being housed view
struct AllTablesView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Table.getAllTables()) var tables: FetchedResults<Table>
#State private var tableTapped: Table = Table()
#State private var isModal = false
var body: some View {
NavigationView {
List {
ForEach(self.tables) { table in
tableCellView(tableNumber: table.number, clean: table.clean, inUse: table.inUse)
.onTapGesture {
self.tableTapped = table
self.isModal = true
print("tapped")
}
}
.onDelete { indexSet in
let deletedTable = self.tables[indexSet.first!]
self.managedObjectContext.delete(deletedTable)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
.sheet(isPresented: $isModal){
TableStatusView(table: self.tableTapped)
.environment(\.managedObjectContext, self.managedObjectContext)
}
}
.navigationBarTitle("All Tables")
.navigationBarItems(leading: EditButton(), trailing:
HStack {
DeleteAllTablesButton()
AddTableButton()
})
}
}
}
And, if for whatever reason this is the issue, here is the code for my view titled "TableStatusView"
struct TableStatusView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Table.getAllTables()) var tables: FetchedResults<Table>
#State var table: Table
var body: some View {
VStack {
Form {
Section {
Text("Table \(table.number)")
}.padding(.all, 10.0)
Section {
Toggle(isOn: $table.inUse) {
Text("In Use")
}.padding(.all, 10.0)
Toggle (isOn: $table.clean) {
Text("Clean")
}.padding(.all, 10.0)
}
}
}
}
}
Is there a reason I'm getting this error, or why my view is being called twice? Is it actually that it's being called twice or am I doing something wrong here? I know certain parts of my code could probably be shrunk down or written better but this is just my first dive into SwiftUI, I haven't had much experience with it outside of basic stuff. Thanks!
I never did find a solution to the question, but I did solve the NavigationLink issue and swapped back to using that. I was using ".onTapGesture" to get a tap gesture and then generate a NavigationLink, thinking it was an action. NavigationLink, I've learned, is actually more or less a container to house content, so I replaced the .onTapGesture and TableCellView() function call with the following:
NavigationLink(destination: TableStatusView(table: table)) {
tableCellView(tableNumber: table.number, clean: table.clean, inUse: table.inUse)
}
And fixed the issue for me. I'm still seeing some errors but from some google-fu found out that they are current bugs of SwiftUI. SwiftUI 2.0 may fix some of these issues
I have an ObservableObject which is supposed to hold my application state:
final class Store: ObservableObject {
#Published var fetchInterval = 30
}
now, that object is being in injected at the root of my hierarchy and then at some component down the tree I'm trying to access it and bind it to a TextField, namely:
struct ConfigurationView: View {
#EnvironmnetObject var store: Store
var body: some View {
TextField("Fetch interval", $store.fetchInterval, formatter: NumberFormatter())
Text("\(store.fetchInterval)"
}
}
Even though the variable is binded (with $), the property is not being updated, the initial value is displayed correctly but when I change it, the textfield changes but the binding is not propagated
Related to the first question, is, how would I receive an event once the value is changed, I tried the following snippet, but nothing is getting fired (I assume because the textfield is not correctly binded...
$fetchInterval
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.sink { interval in
print("sink from my code \(interval)")
}
Any help is much appreciated.
Edit: I just discovered that for text variables, the binding works fine out of the box, ex:
// on store
#Published var testString = "ropo"
// on component
TextField("Ropo", text: $store.testString)
Text("\(store.testString)")
it is only on the int field that it does not update the variable correctly
Edit 2:
Ok I have just discovered that only changing the field is not enough, one has to press Enter for the change to propagate, which is not what I want, I want the changes to propagate every time the field is changed...
For anyone that is interested, this is te solution I ended up with:
TextField("Seconds", text: Binding(
get: { String(self.store.fetchInterval) },
set: { self.store.fetchInterval = Int($0.filter { "0123456789".contains($0) }) ?? self.store.fetchInterval }
))
There is a small delay when a non-valid character is added, but it is the cleanest solution that does not allow for invalid characters without having to reset the state of the TextField.
It also immediately commits changes without having to wait for user to press enter or without having to wait for the field to blur.
Do it like this and you don't even have to press enter. This would work with EnvironmentObject too, if you put Store() in SceneDelegate:
struct ContentView: View {
#ObservedObject var store = Store()
var body: some View {
VStack {
TextField("Fetch interval", text: $store.fetchInterval)
Text("\(store.fetchInterval)")
}
} }
Concerning your 2nd question: In SwiftUI a view gets always updated automatically if a variable in it changes.
how about a simple solution that works well on macos as well, like this:
import SwiftUI
final class Store: ObservableObject {
#Published var fetchInterval: Int = 30
}
struct ContentView: View {
#ObservedObject var store = Store()
var body: some View {
VStack{
TextField("Fetch interval", text: Binding<String>(
get: { String(format: "%d", self.store.fetchInterval) },
set: {
if let value = NumberFormatter().number(from: $0) {
self.store.fetchInterval = value.intValue
}}))
Text("\(store.fetchInterval)").padding()
}
}
}