Animate State change in SwiftUI - swift

I‘m currently playing around with SwiftUI. In SwiftUI it‘s possible, to animate a State change for example like so:
struct Foo: View {
#State private var show = false
var body: some View {
VStack {
if show {
Text("Foo")
}
Button(action: {
withAnimation {
self.show.toggle()
}
}) {
Text(show ? "Hide" : "Show")
}
}
}
}
But if I have for example a TextField:
struct Foo: View {
#State private var text = ""
var body: some View {
VStack {
TextField($text, placeholder: Text("Foo")) {
print("editing ended")
}
if !text.isEmpty {
Button(action: {}) {
Text("Done")
}
}
}
}
}
I‘m not able to find a way to animate this change, because the State property is changed by the TextField without a call to withAnimation().
Is it possible to get this change animated?

Just add the animation modifier to wrap your button
var body: some View {
VStack {
TextField($text, placeholder: Text("Foo")) {
print("editing ended")
}
// if !text.isEmpty {
Button(action: {}) {
Text("Done")
}
.background(text.isEmpty ? Color.red : Color.yellow )
//.animation(.basic(duration: 1))
.animation(Animation.default.speed(1))
}
}
}

TextField("Placeholder", text:$text.animation())
Everything that uses the text will be animated when it is changed.

Related

Unable to fix status bar position when blur or brightness effect is used [SwiftUI]

I am trying to simply make the NavigationView return to normal after either the cancel button or OK button of the custom alert view is clicked. However, the blur effect that I would like to have when the alert view is shown, seems to cause the navigation view to glitch out. I know it's the blur/brightness effect, as with both of these effects, I used trial and error and figured out that this glitch only occurs when either of them are used (brightness modifier not shown -- only the blur effect here). In the gif below, you can see that the navigation view "ignores" the status bar. How do I fix this?
My code:
struct Sidebar: View {
#Binding var documents: Documents
var body: some View {
List(Folder.folders, id: \.id) { folder in
Button {
documents = folder.documents
} label: {
Text(folder.title)
}
}
.navigationTitle("Folder Directory")
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button {
Folder.folders.append(Folder())
} label: {
Image(systemName: "plus")
}
}
}
}
}
struct ContentView: View {
#State private var documents = Documents()
#State private var showDocumentView = false
#State private var showNewDocument = false
#State private var showNewFolder = false
#State private var newDocument = Document()
var body: some View {
ZStack {
NavigationView {
Sidebar(documents: $documents)
ScrollView {
LazyVGrid(columns: [GridItem(), GridItem(), GridItem()]) {
ForEach(documents.list, id: \.self.id) { doc in
FileView(doc)
}
}
}
.navigationTitle("Documents")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
VStack {
NavigationLink(isActive: $showDocumentView) {
NoteView(document: newDocument)
} label: {
EmptyView()
}
Button {
newDocument = Document()
showNewDocument = true
} label: {
Image(systemName: "plus")
}
}
}
}
}
.disabled(showNewDocument)
.blur(radius: showNewDocument ? 30 : 0)
TextAlertView(visibilityBool: $showNewDocument, textInput: $newDocument.title) {
documents.list.append(newDocument)
showDocumentView.toggle()
}
.opacity(showNewDocument ? 1 : 0)
}
.animation(.spring(), value: showNewDocument)
.environmentObject(documents)
}
}
This seems to be a bug with SwiftUI. I've experienced something similar using brightness on NavigationView.
If I use .brightness(configuringFilter ? -0.05 : 0), I lose the navigation bar:
If I use .brightness(configuringFilter ? -0.05 : 0.0001) I get the desired effect:

Unable to rerender a View using onAppear

I am trying to show a pause button if a sound file is playing, i have a uniform source of truth for the sound file, which i can access via ViewModel, now all works well on other Views, but on parent View where all navigation links are, when i go back to it using the back button from other Views, the miniplayer that shows pause disappears...
So i decided that on the .onAppear of NavigationView or text view of parent View i will implement the logic that can detect if a sound file is playing and if so , show a button at bottom to pause the sound file.
Now i can use print and it shows correct value on onAppear in terms of sound file playing or not, but the moment i try to use HStack or any other View to be added i get warning -
Result of 'HStack<Content>' initializer is unused
Now if i decide to use State then also i get similar warning, how can i make the View rerender onAppear, or is that not possible, if that is the case from where i can implement this logic, thanks ....
import Foundation
import SwiftUI
struct HomePageTabView: View {
#Binding var songLVM: SongListVM
#State var miniBar: Bool = false
init(songLVM: Binding<SongListVM>){
self._songLVM = songLVM
UITableView.appearance().backgroundColor = UIColor(.white)
}
var body: some View {
NavigationView {
List {
//Artists
NavigationLink(
destination: ArtistList(songLVM: $songLVM))
{
HStack {
Image(systemName: "music.mic")
Text("Artists")
}
}
//Albums
NavigationLink(
destination: Text("Albums"))
{
HStack {
Image(systemName: "music.note.list")
Text("Albums")
}
}
//Collections
NavigationLink(
//destination: ArtistView())
destination: ArtistViewMain( songLVM: $songLVM))
{
HStack {
Image(systemName: "music.quarternote.3")
Text("Collections")
}
}
//About Us
NavigationLink(
destination: Text("About Us"))
{
HStack {
Image(systemName: "music.note.house.fill")
Text("About Us")
}
}
//Contact Us
NavigationLink(
destination: ArtistView())
{
HStack {
Image(systemName: "phone.circle")
Text("Contact Us")
}
}
}
}
.onAppear {
if(songLVM.audioPlayer?.isPlaying != nil){
HStack {
Button("Stop") {
songLVM.audioPlayer?.stop()
}
}
}
}
}
}
I had also tried
.onAppear{
miniBar.toggle()
if(miniBar == true){
HStack {
Text("Stop")
}
}
}
but got Result of 'HStack<Content>' initializer is unused
I will give easy and basic template for working with swift's ui states.
You can refer it and add your views or navigation link.
struct YourView: View {
/// If you want to pass it on init, use #ObservedObject instead
/// https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-observedobject-state-and-environmentobject
#StateObject var viewModel = YourViewModel()
var body: some View {
NavigationView {
VStack {
if viewModel.isPlaying {
Button {
viewModel.stop()
} label: {
Text("Stop")
}
} else {
Button {
viewModel.start()
} label: {
Text("Start")
}
}
Toggle(isOn: $viewModel.isPlaying) {
Text("isPlaying")
}
}
}
.onAppear {
viewModel.transform()
}
}
}
class YourViewModel: ObservableObject {
#Published var isPlaying = false
func transform() {
fetchStatus()
}
func fetchStatus() {
isPlaying = true
}
func stop() { isPlaying = false }
func start() { isPlaying = true }
}

Modify state in nested object

im new to Swift and i am having this issue. I want to make a play/pause button in the toolbar and i decided to move the toolbar code to its own object Toolbar. The button should change its image when pressed but when i press it the state doesn't change. What am i doing wrong?
struct ContentView: View {
var body: some View {
NavigationView {
List{
Text("asdf")
}
.toolbar {
Toolbar()
}
}
}
}
struct Toolbar: ToolbarContent {
#State var started = false
var body: some ToolbarContent {
ToolbarItem(id:"start-button", placement: .primaryAction) {
Button(action: {
self.started.toggle()
}) {
Image(systemName: self.started == true ? "pause.fill" : "play.fill")
.foregroundColor(.white)
}
}
}
}
Use two-way binding.
struct ContentView: View {
#State private var started = false //<-- Here
var body: some View {
NavigationView {
List{
Text("asdf")
}
.toolbar {
Toolbar(started: $started) //<-- Here
}
}
}
}
struct Toolbar: ToolbarContent {
#Binding var started: Bool //<-- Here
var body: some ToolbarContent {
ToolbarItem(id:"start-button", placement: .primaryAction) {
Button(action: {
self.started.toggle()
}) {
Image(systemName: self.started ? "pause.fill" : "play.fill")
.foregroundColor(.white)
}
}
}
}
#State only works inside Views, and ToolbarContent isn't a View.
You should keep the #State started inside ContentView, and pass in its wrapped value to the toolbar. Then, use a closure to update it.
struct ContentView: View {
#State var started = false
var body: some View {
NavigationView {
List{
Text("asdf")
}
.toolbar {
Toolbar(started: started) {
started.toggle() /// executed when `pressed` is called
}
}
}
}
}
struct Toolbar: ToolbarContent {
var started: Bool
var pressed: (() -> Void) /// closure here!
var body: some ToolbarContent {
ToolbarItem(id:"start-button", placement: .primaryAction) {
Button(action: {
pressed() /// call the closure
}) {
Image(systemName: self.started == true ? "pause.fill" : "play.fill")
.foregroundColor(.white)
}
}
}
}

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