Animating date changes in SwiftUI - animation

I am struggling to create an animation of a changing date value. A simplified example is below. It is intended to update the Text view with changing values as the date is updated from its original value to its new value. Help would be most appreciated!
struct DateAnimationView: View {
#State var date = Date()
typealias AnimatableData = Date
var animatableData: Date{
get{date}
set{date = newValue}
}
var body: some View {
VStack{
Text(date, style: .time)
.padding()
Button(action:{
withAnimation(.linear(duration: 3.0)){
date.addTimeInterval(60 * 60 * 3)
}
}){
Text("Add 3 hours")
}
.padding()
Spacer()
}
}
}
Another attempt which also fails:
struct InnerView: View {
var date: Date
var interval: TimeInterval
typealias AnimatableData = TimeInterval
var animatableData: TimeInterval{
get{interval}
set{interval = newValue}
}
var body: some View{
Text(date.addingTimeInterval(interval), style: .time)
}
}
struct DateAnimationView: View{
#State var isOn: Bool = false
var body: some View{
VStack{
InnerView(date: Date(), interval: isOn ? 60*60*3 : 0)
Button(action:{
isOn.toggle()
}){
Text("Go")
}
}
}
}

The essential idea is that you need to specify an id for the component you're animating. SwiftUI uses this id to understand if it's the same component or a new component when doing a redraw. If it is a new component, it will remove the old one and add a new with the animation. The code below achieves the animation.
struct DateAnimationView: View {
#State var date = Date()
typealias AnimatableData = Date
var animatableData: Date{
get{date}
set{date = newValue}
}
var body: some View {
VStack{
Text(date, style: .time)
.padding()
.id(String(date.hashValue))
Button(action:{
withAnimation(.linear(duration: 3.0)){
date.addTimeInterval(60 * 60 * 3)
}
}){
Text("Add 3 hours")
}
.padding()
Spacer()
}
}
}
A similar issue is solved by the solution posted here.

Related

SwiftUI - Using a Picker on another view to sort results from API call

I have a view that makes an API call to pull nearby restaurant data and display it in a list. I've placed a button in the navigation bar of that view to display a sheet which will ultimately show the user filter options. For now, it uses a Picker placed in a form to allow the user to pick how they want to sort the results. I am using a variable with a #Binding property wrapper in the filter view to pass the selected value to a #State variable in the restaurant data view (selectedOption). It all works great until the view is reloaded. Either by going to a different view or relaunching the app. It appears the selectedOption variable in my API call in the onAppear function of the restaurant data view is being reset to what I've set the default value to when I defined the #State variable. I am wondering if there is a way to persist the value of what was chosen in the filter view through view reloads of the restaurant data view.
Restaurant data view:
import SwiftUI
import SwiftyJSON
import SDWebImageSwiftUI
struct RestaurantsView: View {
#EnvironmentObject var locationViewModel: LocationViewModel
#EnvironmentObject var venueDataViewModel: VenueDataViewModel
#State var selectedOption: String = "rating"
#State private var showingSheet = false
#State private var searchText = ""
#State private var showCancelButton: Bool = false
var body: some View {
let CPLatitude: Double = locationViewModel.lastSeenLocation?.coordinate.latitude ?? 0.00
let CPLongitude: Double = locationViewModel.lastSeenLocation?.coordinate.longitude ?? 0.00
GeometryReader { geometry in
VStack {
Text("Restaurants")
.padding()
List(venueDataViewModel.venuesListTen) { index in
NavigationLink(destination: RestaurantsDetailView(venue: index)) {
HStack {
VStack(alignment: .leading, spacing: 6) {
Text(index.name ?? "")
.font(.body)
.lineLimit(2)
Text(index.address ?? "")
.font(.subheadline)
.lineLimit(2)
}
}
}
Spacer()
if index.image != nil {
WebImage(url: URL(string: index.image ?? ""), options: .highPriority, context: nil)
.resizable()
.frame(width: 70, height: 70, alignment: .center)
.aspectRatio(contentMode: .fill)
.cornerRadius(12)
}
}
Text("Selected: \(selectedOption)")
Spacer()
}.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
showingSheet.toggle()
}, label: {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}
)
.sheet(isPresented: $showingSheet, content: {
NavigationView {
RestaurantsFilterView(selectedOption: self.$selectedOption)
}
})
}
}
.onAppear {
venueDataViewModel.retrieveVenues(latitude: CPLatitude, longitude: CPLongitude, category: "restaurants", limit: 50, sortBy: selectedOption, locale: "en_US") { (response, error) in
if let error = error {
print("\(error)")
}
}
}
}
}
}
Filter view:
import SwiftUI
struct RestaurantsFilterView: View {
#EnvironmentObject var locationViewModel: LocationViewModel
#EnvironmentObject var venueDataViewModel: VenueDataViewModel
var sortOptions = ["rating", "review_count"]
#Binding var selectedOption: String
var body: some View {
let CPLatitude: Double = locationViewModel.lastSeenLocation?.coordinate.latitude ?? 0.00
let CPLongitude: Double = locationViewModel.lastSeenLocation?.coordinate.longitude ?? 0.00
VStack {
Text("Filter")
Form {
Section {
Picker(selection: $selectedOption, label: Text("Sort")) {
ForEach(sortOptions, id: \.self) {
Text($0)
}
}.onChange(of: selectedOption) { Equatable in
venueDataViewModel.retrieveVenues(latitude: CPLatitude, longitude: CPLongitude, category: "restaurants", limit: 50, sortBy: selectedOption, locale: "en_US") { (response, error) in
if let error = error {
print("\(error)")
}
}
}
}
}
}
}
}
I am still new to Swift and SwiftUI so I appreciate the help.
Thank you!

SwiftUI presenting sheet with Binding variable doesn't work when first presented

I'm trying to present a View in a sheet with a #Binding String variable that just shows/binds this variable in a TextField.
In my main ContentView I have an Array of Strings which I display with a ForEach looping over the indices of the Array, showing a Button each with the text of the looped-over-element.
The Buttons action is simple: set an #State "index"-variable to the pressed Buttons' Element-index and show the sheet.
Here is my ContentView:
struct ContentView: View {
#State var array = ["first", "second", "third"]
#State var showIndex = 0
#State var showSheet = false
var body: some View {
VStack {
ForEach (0 ..< array.count, id:\.self) { i in
Button("\(array[i])") {
showIndex = i
showSheet = true
}
}
// Text("\(showIndex)") // if I uncomment this line, it works!
}
.sheet(isPresented: $showSheet, content: {
SheetView(text: $array[showIndex])
})
.padding()
}
}
And here is the SheetView:
struct SheetView: View {
#Binding var text: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text)
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
The problem is, when I first open the app and press on the "second" Button, the sheet opens and displays "first" in the TextField. I can then dismiss the Sheet and press the "second" Button again with the same result.
If I then press the "third" or "first" Button everything works from then on. Pressing any Button results in the correct behaviour.
Preview
Interestingly, if I uncomment the line with the Text showing the showIndex-variable, it works from the first time on.
Is this a bug, or am I doing something wrong here?
You should use custom Binding, custom Struct for solving the issue, it is complex issue. See the Example:
struct ContentView: View {
#State private var array: [String] = ["first", "second", "third"]
#State private var customStruct: CustomStruct?
var body: some View {
VStack {
ForEach (array.indices, id:\.self) { index in
Button(action: { customStruct = CustomStruct(int: index) }, label: {
Text(array[index]).frame(width: 100)
})
}
}
.frame(width: 300, height: 300, alignment: .center)
.background(Color.gray.opacity(0.5))
.sheet(item: $customStruct, content: { item in SheetView(text: Binding.init(get: { () -> String in return array[item.int] },
set: { (newValue) in array[item.int] = newValue }) ) })
}
}
struct CustomStruct: Identifiable {
let id: UUID = UUID()
var int: Int
}
struct SheetView: View {
#Binding var text: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text)
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
I had this happen to me before. I believe it is a bug, in that until it is used in the UI, it doesn't seem to get set in the ForEach. I fixed it essentially in the same way you did, with a bit of subtlety. Use it in each Button as part of the Label but hide it like so:
Button(action: {
showIndex = i
showSheet = true
}, label: {
HStack {
Text("\(array[i])")
Text(showIndex.description)
.hidden()
}
})
This doesn't change your UI, but you use it so it gets properly updated. I can't seem to find where I had the issue in my app, and I have changed the UI to get away from this, but I can't remember how I did it. I will update this if I can find it. This is a bit of a kludge, but it works.
Passing a binding to the index fix the issue like this
struct ContentView: View {
#State var array = ["First", "Second", "Third"]
#State var showIndex: Int = 0
#State var showSheet = false
var body: some View {
VStack {
ForEach (0 ..< array.count, id:\.self) { i in
Button(action:{
showIndex = i
showSheet.toggle()
})
{
Text("\(array[i])")
}.sheet(isPresented: $showSheet){
SheetView(text: $array, index: $showIndex)
}
}
}
.padding()
}
}
struct SheetView: View {
#Binding var text: [String]
#Binding var index: Int
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text[index])
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
In SwiftUI2 when calling isPresented if you don't pass bindings you're going to have some weird issues.
This is a simple tweak if you want to keep it with the isPresented and make it work but i would advise you to use the item with a costum struct like the answer of swiftPunk
This is how I would do it. You'll lose your form edits if you don't use #State variables.
This Code is Untested
struct SheetView: View {
#Binding var text: String
#State var draft: String
#Environment(\.presentationMode) var presentationMode
init(text: Binding<String>) {
self._text = text
self._draft = State(initialValue: text.wrappedValue)
}
var body: some View {
VStack {
TextField("text:", text: $draft)
Button("dismiss") {
text = draft
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}

why is a passed parameter displaying the previous content of an EnvironmentObject

this is a Macos app where the parsclass is setup in a previous view that contains the YardageRowView below. That previous view is responsible for changing the contents of the parsclass. This is working is other views that use a NavigationLink to display the views.
When the parsclass is changed, this view is refreshed, but the previous value is put in the text field on the holeValueTestView.
I cannot comprehend how the value is not being passed into the holeValueTestView correctly
This is a view shown as a .sheet, and if I dismiss it and display it again, everything is fine.
if you create a macOS project called YardageSample and replace the ContentView.swift and YardageSampleApp.swift with the two files below, you can see that the display in red changes and the black numbers do not change until you click Done and redisplay the .sheet
//
// YardageSampleApp.swift
// YardageSample
//
// Created by Brian Quick on 2021-04-12.
//
import SwiftUI
#main
struct YardageSampleApp: App {
#StateObject var parsclass = parsClass()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(parsclass)
}
}
}
//
// ContentView.swift
// YardageSample
//
// Created by Brian Quick on 2021-04-12.
//
import SwiftUI
struct ContentView: View {
#StateObject var parsclass = parsClass()
enum ActiveSheet : String , Identifiable {
case CourseMaintenance
var id: String {
return self.rawValue
}
}
#State var activeSheet : ActiveSheet? = nil
var body: some View {
Button(action: {
self.activeSheet = .CourseMaintenance
}) {
Text("Course Maintenance")
}
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .CourseMaintenance:
CourseMaintenance()
}
}.frame(width: 200, height: 200, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
}
}
class parsClass: ObservableObject {
#Published var pars = [parsRec]()
init() {
self.pars = [parsRec]()
self.pars.append(parsRec())
}
func create(newpars: [parsRec]) {
pars.removeAll()
pars = newpars
}
}
class parsRec: Identifiable, Codable {
var id = UUID()
var Hole = 1
var Yardage = 1
}
struct CourseMaintenance: View {
#EnvironmentObject var parsclass: parsClass
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Button(action: {presentationMode.wrappedValue.dismiss()}, label: {
Text("Done")
})
Button(action: {switchScores(number: 1)}, label: {
Text("Button 1")
})
Button(action: {switchScores(number: 2)}, label: {
Text("Button 2")
})
Button(action: {switchScores(number: 3)}, label: {
Text("Button 3")
})
CourseDetail().environmentObject(parsclass)
}.frame(width: 400, height: 400, alignment: .center)
}
func switchScores(number: Int) {
var newparRecs = [parsRec]()
for i in 0..<17 {
let myrec = parsRec()
myrec.Hole = i
myrec.Yardage = number
newparRecs.append(myrec)
}
parsclass.create(newpars: newparRecs)
}
}
struct CourseDetail: View {
#EnvironmentObject var parsclass: parsClass
var body: some View {
HStack(spacing: 0) {
ForEach(parsclass.pars.indices, id: \.self) { indice in
// this displays the previous value
holeValueTestView(value: String(parsclass.pars[indice].Yardage))
// this displays the correct value after parsclass has changed
Text(String(parsclass.pars[indice].Yardage))
.foregroundColor(.red)
}
}
}
}
struct holeValueTestView: View {
#State var value: String
var body: some View {
//TextField(String(value), text: $value)
Text(value)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
There are a couple of issues going on:
You have multiple instances of parsClass. One is defined in YardageSampleApp and passed into the view hierarchy as a #EnvironmentObject. The second is defined in ContentView as a #StateObject. Make sure you're only using one.
On holeValueTestView, you defined value as a #State variable. That gets set initially when the view is created by its parent and then it maintains its own state. So, when the environmentObject changed, because it was in charge of its own state at this point, it didn't update the value. You can simply remove #State and see the behavior that you want.
struct ContentView: View {
#EnvironmentObject var parsclass : parsClass //<-- Here
enum ActiveSheet : String , Identifiable {
case CourseMaintenance
var id: String {
return self.rawValue
}
}
#State var activeSheet : ActiveSheet? = nil
var body: some View {
Button(action: {
self.activeSheet = .CourseMaintenance
}) {
Text("Course Maintenance")
}
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .CourseMaintenance:
CourseMaintenance()
}
}.frame(width: 200, height: 200, alignment: .center)
}
}
struct holeValueTestView: View {
var value: String //<-- Here
var body: some View {
Text(value)
}
}
As a side note:
In Swift, normally type names are capitalized. If you want to write idiomatic Swift, you would change your parsClass to ParsClass for example.

what's problem with swiftui sheet under macos

my code works under the ios,but under macos , the sheet will repeat "show close" loop auto. when I click the button edit ,the sheet shows on the top of the window,then I click the button close on the sheet,the sheet disappeared,but appear again immediately,and close auto,apear auto ,a loop
main view code:
struct NewWordsView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: WordFrequency.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \WordFrequency.bookName, ascending: true)])
var words:FetchedResults<WordFrequency>
#State var showDetail:Bool = false
#State var wordIndex:Int = 0
var body: some View {
List{
ForEach(self.words.indices, id:\.self){ idx in
HStack{
Button(action: {self.deleteWords(idx:idx)}){
Text("X")
}
Button(action:{
self.showDetail.toggle()
self.wordIndex = idx
}){
Text("edit")
}
Text(self.words[idx].word!)
.font(.title)
}
}
.sheet(isPresented: self.$showDetail){
EditWordView( showDetail: self.$showDetail,word: self.words[self.wordIndex])
}
}
}
subview code:
import SwiftUI
struct EditWordView: View {
#Environment(\.managedObjectContext) var context
#Binding var showDetail:Bool
let word:WordFrequency
#State var chinese:String = ""
var body: some View {
VStack{
Button(action:{
self.showDetail.toggle()
}){
Text("close")
}
Text("\(word.word ?? "")")
.font(.title)
TextField("中文解释", text: self.$chinese)
.font(.title)
}
}
}
the following code should after List view, not ForEach.
.sheet(isPresented: self.$showDetail){
EditWordView( showDetail: self.$showDetail,word: self.words[self.wordIndex])
}

SwiftUI how to get an event if #Published var changes

I use the library RKmanager to show a calendar with a dateRangePicker. But i can't get an event if the parameter rkManager.endDate is set from nil to a date.
I want activate and deactivate an addButton for the dateRange if a range is set.
The Object from i want to get the trigger if changes:
class RKManager : ObservableObject {
#Published var startDate: Date! = nil
#Published var endDate: Date! = nil
Now my View:
import SwiftUI
struct DateRangeView: View {
#State var rkManager = RKManager(calendar: Calendar.current, minimumDate: Date(), maximumDate: Date().addingTimeInterval(60*60*24*365), mode: 1)
#State private var addNewDateRangeIsDisabled: Bool = true
var body: some View {
VStack(alignment: .leading) {
RKViewController(isPresented: .constant(false), rkManager: self.rkManager )
Button(action: {
if self.rkManager.endDate != nil
&& self.rkManager.startDate != nil
{
self.addNewDateRange()
}
}) {
HStack {
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 20, height: 20)
Text("Add new DateRange")
}
}
.padding()
.disabled(self.rkManager.endDate == nil) // --> here i want to change if the value changed
}.navigationBarTitle("TestRoomName", displayMode: .inline)
}
A ObservableObject should be represented in view by ObservedObject dynamic property wrapper so view would be updated on published property change.
Here is fix
struct DateRangeView: View {
#ObservedObject var rkManager = RKManager(calendar: Calendar.current, minimumDate: Date(), maximumDate: Date().addingTimeInterval(60*60*24*365), mode: 1)
// ... other code

Resources