Problem with SwiftUI DatePicker animation - datepicker

I'm trying to create a Wheel date picker style that will animate in from the bottom of the screen when a button is clicked, however the animation seems to lag when the date picker moves and it looks awful.
Is there anything I could add to help the animation look smoother. I've tested this code on the simulator and my device and both seem to have the same issue.
struct datepicker: View {
#State private var date = Date()
#State private var shown: Bool = false
#State private var isHidden = false
var body: some View {
VStack{
Button("toggle",action:{
shown.toggle()
})
DatePicker("", selection: $date)
.labelsHidden()
.datePickerStyle(WheelDatePickerStyle())
.offset(y: shown ? 500:0)
.animation(.linear)
.transition(.move(edge: .bottom))
}
}
}

Usually there's no need to use offset in SwiftUI. Here you can rely on the .transition(.move(edge: .bottom)) when the view is added/removed from the view:
struct datepicker: View {
#State private var date = Date()
#State private var shown: Bool = false
#State private var isHidden = false
var body: some View {
VStack {
Button("toggle", action: {
shown.toggle()
})
ZStack {
DatePicker("", selection: .constant(Date()))
.labelsHidden()
.datePickerStyle(WheelDatePickerStyle())
.opacity(0)
if shown {
DatePicker("", selection: $date)
.labelsHidden()
.datePickerStyle(WheelDatePickerStyle())
.animation(.linear)
.transition(.move(edge: .bottom))
}
}
}
}
}
(I added an extra hidden DatePicker so other controls don't change their position during transition.)

Related

Contracting an expanded DatePicker on SwiftUI / MacOS

Is there a way to get a DatePicker that has been expanded to contract when the user clicks on a date?
The following code will crash the app - or at least put it into the locked state.
There is a Text component showing the date. When you click on it the DatePicker appears in its place - but there is no way to dismiss it after it is an expanded mode. Trying to swap back to the original Text, by setting edit mode to false when the date changes effectively locks the app:
struct ContentView: View {
#State private var date: Date = .now
#State private var editMode: Bool = false
var body: some View {
VStack {
if editMode {
DatePicker("Test", selection: $date)
.labelsHidden()
.onChange(of: date) { _ in
editMode = false
}
.frame(width: 50)
} else {
Text(date.formatted(date: .abbreviated, time: .omitted))
.onTapGesture {
editMode = true
}
}
}
.padding()
}
}
Ideally I'd also be able to dismiss it programatically.
I have tried using a ZStack with opacity control, such as:
struct ContentView: View {
#State private var date: Date = .now
#State private var editMode: Bool = false
var body: some View {
ZStack {
DatePicker("Test", selection: $date)
.labelsHidden()
.onChange(of: date) { _ in
editMode = false
}
.frame(width: 50)
.opacity(editMode ? 1.0 : 0.0)
Text(date.formatted(date: .abbreviated, time: .omitted))
.onTapGesture {
editMode = true
}
.opacity(editMode ? 0.0 : 1.0)
}
.padding()
}
}
But this isn't working in my actual use case. I'd really like to know if there was a way of dismissing that DatePicker expanded form.

#AppStorage property wrapper prevents from dismissing views

I have an app with four (4) views, on the first view I'm showing a list of cars pulled from CoreData, the second view is presented when a car is tapped and it shows the services for each car. The third view is presented when tapping on a service, and it shows the details of the selected service. The fourth view is presented when tapping a button and it shows records for the specified service.
The issue I'm having is that for some reason if I use an #AppStorage property wrapper within the ServicesView I cannot dismiss the fourth view (RecordsView). I don't think the issue is with CoreData but let me know if you need to see the code for Core Data.
Any idea why adding an #AppStorage property wrapper in the ServicesView would affect other views?
CarsView
struct CarsView: View {
#ObservedObject var carViewModel:CarViewModel
#State private var carInfoIsPresented = false
var body: some View {
NavigationView{
VStack{
List {
ForEach(carViewModel.cars) { car in
HStack{
VStack(alignment:.leading){
Text(car.model ?? "")
.font(.title2)
Text(car.make ?? "")
.foregroundColor(Color(UIColor.systemGray))
}
NavigationLink(destination: ServicesView(carViewModel: carViewModel, selectedCar: car)){
Spacer()
Text("Services")
.frame(width: 55)
.font(.caption)
.foregroundColor(Color.systemGray)
}
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Cars")
.accentColor(.white)
.padding(.top, 20)
}
}
}
}
ServicesView
struct ServicesView: View {
#ObservedObject var carViewModel: CarViewModel
var selectedCar: Car
// ISSUE: No issues dismissing the RecordsView if I comment this out
#AppStorage("sortByNameKey") private var sortByName = true
#State private var selectedService: CarService?
var body: some View {
VStack{
List {
ForEach(carViewModel.carServices) { service in
HStack{
Text(service.name ?? "")
.font(.title3)
NavigationLink(destination: ServiceInfoView(carViewModel: carViewModel, selectedCar: selectedCar, selectedService: service)){
Spacer()
Text("Details")
.font(.caption)
.foregroundColor(Color.systemGray)
}
}
}
}
.navigationBarTitle(Text("\(selectedCar.model ?? "Services") - Services"))
.listStyle(GroupedListStyle())
}
.onAppear{
carViewModel.getServices(forCar: selectedCar)
}
}
}
ServiceInfoView
struct ServiceInfoView: View {
#ObservedObject var carViewModel: CarViewModel
#State private var recordsViewIsPresented = false
#State var selectedCar: Car
#State var selectedService: CarService
var body: some View {
VStack{
Text(selectedService.name ?? "")
.font(.largeTitle)
.padding(.bottom)
VStack{
Button(action: openRecordsView) {
Text("Service History")
}
.padding(10)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(15)
}
}
.sheet(isPresented: $recordsViewIsPresented){
RecordsView(carViewModel: carViewModel, selectedService: selectedService)
}
}
func openRecordsView(){
recordsViewIsPresented.toggle()
}
}
RecordsView
struct RecordsView: View {
#ObservedObject var carViewModel: CarViewModel
#State var selectedService: CarService
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack{
List {
Section(header: Text("Records")) {
ForEach(carViewModel.serviceRecords) { record in
HStack{
Text("Service Date:")
Text("\(record.serviceDate ?? Date(), style: .date)")
.foregroundColor(Color(UIColor.systemGray))
}
}
}
}
.background(Color.purple)
.listStyle(GroupedListStyle())
}
.navigationBarTitle("Records for \(selectedService.name ?? "")", displayMode: .inline)
.navigationBarItems(leading: Button("Cancel", action: dismissView))
.onAppear{
carViewModel.getRecords(forService: selectedService)
}
}
}
func dismissView(){
presentationMode.wrappedValue.dismiss()
}
}
NavigationView can only push one detail screen unless you set .isDetailLink(false) on the NavigationLink.
FYI we don't use view model objects in SwiftUI, you have to learn to use the View struct correctly along with #State, #Binding, #FetchRequest etc. that make the safe and efficient struct behave like an object. If you ignore this and use an object you'll experience the bugs that Swift with its value types was designed to prevent. For more info see this answer MVVM has no place in SwiftUI.

SwiftUI keyboard-shortcut without modifier doesn't work in popover

In a View with a textfield I have a popover, activated by a button.
In that popover I want to listen to a keyboardShortcut without modifiers.
These keypresses arrive in the textfild of the parent view instead of the popover.
What can I do, to react on these in the popover?
struct ContentView: View {
#State var showPopOver = false
#State var text = ""
var body: some View {
VStack{
TextField("enter text here", text: $text)
Button("show popover"){ showPopOver = true }
.popover(isPresented: $showPopOver) {PopOverView(text: $text)} }
.keyboardShortcut("p")
.padding()
}
}
struct PopOverView: View {
#Environment(\.dismiss) var dismiss
#Binding var text: String
var body: some View {
VStack{
Text("my Popover")
Button("set Tom"){
text = "Tom"
dismiss()
}
.keyboardShortcut("t",modifiers: [])
//.keyboardShortcut("t") // like this it works
Button("set Frank"){
text = "Frank"
dismiss()
}
.keyboardShortcut("f",modifiers: [])
}
.padding()
}
}
KeyboardShortcuts with a modifier in the popover do work (commented out).

SwiftUI TextField doesn't commit change when tapping another TextField

I am building some basic form functionality in my app at the moment and I am having trouble with TextFields not changing the value in the relevant binded variable when tapping another TextField or pressing "Done" in Edit Mode.
#Binding var jobDetails: JobDetails
#Environment(\.colorScheme) var colorScheme: ColorScheme
...
var body: some View {
...
HStack {
Text("Hourly Rate")
Spacer()
TextField("", value: $jobDetails.hourlyRateBasic, formatter: TextFormatters().currencyFormatter())
.keyboardType(.asciiCapableNumberPad)
.multilineTextAlignment(.trailing)
...
In the iOS simulator, the field only seems to update when I physically hit the return key on my keyboard after typing in a new value (not the soft keyboard in the simulator). I would like the TextField to commit it's change to jobDetails.hourlyRateBasic when tapping another TextField or pressing "Done" to exit edit mode.
It seems that onEditingChanged fires when I tap another TextField, but I don't know how to leverage that into changing the jobDetails with the new value.
This is typical behavior of TextField in SwiftUI. Following is an example of it and alternative method to make TextField more responsive while typing.
import SwiftUI
struct ContentView: View {
#State private var text: String = "0"
#State private var num: Int = 0
private var resultString: String {
if let num = Int(self.text) {
return String(num*num)
}
return "0"
}
private var resultInt: Int {
return self.num*self.num
}
var body: some View {
VStack {
VStack(alignment:.leading) {
Text("Input number as String")
TextField("String Number",text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("Input number as Int")
TextField("Int Number", value: self.$num, formatter: NumberFormatter())
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Spacer()
Text("From String")
Text("Square of \(self.text) is \(self.resultString)") .font(.title)
Spacer()
Text("From Int")
Text("Square of \(self.num) is \(self.resultInt)") .font(.title)
Spacer()
}.padding()
}
}
This is already fixed. Tested with Xcode 13.3 / iOS 15.4
struct TestView: View {
#State private var value1 = 1.0
#State private var text = ""
private var currencyFormatter: NumberFormatter = {
var nf = NumberFormatter()
nf.numberStyle = .currency
return nf
}()
var body: some View {
VStack {
HStack {
Text("Hourly Rate [\(value1)]")
Spacer()
TextField("", value: $value1, formatter: currencyFormatter)
.keyboardType(.asciiCapableNumberPad)
.multilineTextAlignment(.trailing)
}
HStack {
Text("Other")
Spacer()
TextField("Enter something", text: $text)
}
}
}
}

SwiftUI: Global Overlay That Can Be Triggered From Any View

I'm quite new to the SwiftUI framework and I haven't wrapped my head around all of it yet so please bear with me.
Is there a way to trigger an "overlay view" from inside "another view" when its binding changes? See illustration below:
I figure this "overlay view" would wrap all my views. I'm not sure how to do this yet - maybe using ZIndex. I also guess I'd need some sort of callback when the binding changes, but I'm also not sure how to do that either.
This is what I've got so far:
ContentView
struct ContentView : View {
#State private var liked: Bool = false
var body: some View {
VStack {
LikeButton(liked: $liked)
}
}
}
LikeButton
struct LikeButton : View {
#Binding var liked: Bool
var body: some View {
Button(action: { self.toggleLiked() }) {
Image(systemName: liked ? "heart" : "heart.fill")
}
}
private func toggleLiked() {
self.liked = !self.liked
// NEED SOME SORT OF TOAST CALLBACK HERE
}
}
I feel like I need some sort of callback inside my LikeButton, but I'm not sure how this all works in Swift.
Any help with this would be appreciated. Thanks in advance!
It's quite easy - and entertaining - to build a "toast" in SwiftUI!
Let's do it!
struct Toast<Presenting>: View where Presenting: View {
/// The binding that decides the appropriate drawing in the body.
#Binding var isShowing: Bool
/// The view that will be "presenting" this toast
let presenting: () -> Presenting
/// The text to show
let text: Text
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.presenting()
.blur(radius: self.isShowing ? 1 : 0)
VStack {
self.text
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
Explanation of the body:
GeometryReader gives us the preferred size of the superview , thus allowing the perfect sizing for our Toast.
ZStack stacks views on top of each other.
The logic is trivial: if the toast is not supposed to be seen (isShowing == false), then we render the presenting view. If the toast has to be presented (isShowing == true), then we render the presenting view with a little bit of blur - because we can - and we create our toast next.
The toast is just a VStack with a Text, with custom frame sizing, some design bells and whistles (colors and corner radius), and a default slide transition.
I added this method on View to make the Toast creation easier:
extension View {
func toast(isShowing: Binding<Bool>, text: Text) -> some View {
Toast(isShowing: isShowing,
presenting: { self },
text: text)
}
}
And a little demo on how to use it:
struct ContentView: View {
#State var showToast: Bool = false
var body: some View {
NavigationView {
List(0..<100) { item in
Text("\(item)")
}
.navigationBarTitle(Text("A List"), displayMode: .large)
.navigationBarItems(trailing: Button(action: {
withAnimation {
self.showToast.toggle()
}
}){
Text("Toggle toast")
})
}
.toast(isShowing: $showToast, text: Text("Hello toast!"))
}
}
I used a NavigationView to make sure the view fills the entire screen, so the Toast is sized and positioned correctly.
The withAnimation block ensures the Toast transition is applied.
How it looks:
It's easy to extend the Toast with the power of SwiftUI DSL.
The Text property can easily become a #ViewBuilder closure to accomodate the most extravagant of the layouts.
To add it to your content view:
struct ContentView : View {
#State private var liked: Bool = false
var body: some View {
VStack {
LikeButton(liked: $liked)
}
// make it bigger by using "frame" or wrapping it in "NavigationView"
.toast(isShowing: $liked, text: Text("Hello toast!"))
}
}
How to hide the toast afte 2 seconds (as requested):
Append this code after .transition(.slide) in the toast VStack.
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
self.isShowing = false
}
}
}
Tested on Xcode 11.1
I modified Matteo Pacini's great answer, above, incorporating comments to have the Toast fade in and fade out after a delay. I also modified the View extension to be a bit more generic, and to accept a trailing closure similar to the way .sheet works.
ContentView.swift:
struct ContentView: View {
#State private var lightsOn: Bool = false
#State private var showToast: Bool = false
var body: some View {
VStack {
Button(action: {
if (!self.showToast) {
self.lightsOn.toggle()
withAnimation {
self.showToast = true
}
}
}){
Text("switch")
} //Button
.padding(.top)
Image(systemName: self.lightsOn ? "lightbulb" : "lightbulb.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.all)
.toast(isPresented: self.$showToast) {
HStack {
Text("Lights: \(self.lightsOn ? "ON" : "OFF")")
Image(systemName: self.lightsOn ? "lightbulb" : "lightbulb.fill")
} //HStack
} //toast
} //VStack
} //body
} //ContentView
View+Toast.swift:
extension View {
func toast<Content>(isPresented: Binding<Bool>, content: #escaping () -> Content) -> some View where Content: View {
Toast(
isPresented: isPresented,
presenter: { self },
content: content
)
}
}
Toast.swift:
struct Toast<Presenting, Content>: View where Presenting: View, Content: View {
#Binding var isPresented: Bool
let presenter: () -> Presenting
let content: () -> Content
let delay: TimeInterval = 2
var body: some View {
if self.isPresented {
DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
withAnimation {
self.isPresented = false
}
}
}
return GeometryReader { geometry in
ZStack(alignment: .bottom) {
self.presenter()
ZStack {
Capsule()
.fill(Color.gray)
self.content()
} //ZStack (inner)
.frame(width: geometry.size.width / 1.25, height: geometry.size.height / 10)
.opacity(self.isPresented ? 1 : 0)
} //ZStack (outer)
.padding(.bottom)
} //GeometryReader
} //body
} //Toast
With this you could toast Text, or an Image (or both, as shown below), or any other View.
here is the how to overlay on all of your views including NavigationView!
create a class model to store your views!
class ParentView:ObservableObject {
#Published var view:AnyView = AnyView(EmptyView())
}
create the model in your parrent view and call it in your view hierarchy
pass this class to your environment object of your parent view
struct Example: View {
#StateObject var parentView = ParentView()
var body: some View {
ZStack{
NavigationView{
ChildView()
.environmentObject(parentView)
.navigationTitle("dynamic parent view")
}
parentView.view
}
}
}
from now on you can call parentview in your child view by
#EnvironmentObject var parentView:ParentView
then for example in your tap gesture, you can change the parent view and show a pop up that covers everything including your navigationviews
#StateObject var parentView = ParentView()
here is the full solution copy and play with it in your preview!
import SwiftUI
class ParentView:ObservableObject {
#Published var view:AnyView = AnyView(EmptyView())
}
struct example: View {
#StateObject var parentView = ParentView()
var body: some View {
ZStack{
NavigationView{
ChildView()
.environmentObject(parentView)
.navigationTitle("dynamic parent view")
}
parentView.view
}
}
}
struct ChildView: View {
#EnvironmentObject var parentView:ParentView
var body: some View {
ZStack{
Text("hello")
.onTapGesture {
parentView.view = AnyView(Color.red.opacity(0.4).ignoresSafeArea())
}
}
}
}
struct example_Previews: PreviewProvider {
static var previews: some View {
example()
}
}
also you can improve this dramatically like this...!
struct ParentViewModifire:ViewModifier {
#EnvironmentObject var parentView:ParentView
#Binding var presented:Bool
let anyView:AnyView
func body(content: Content) -> some View {
content
.onChange(of: presented, perform: { value in
if value {
parentView.view = anyView
}
})
}
}
extension View {
func overlayAll<Overlay>(_ overlay: Overlay, presented: Binding<Bool>) -> some View where Overlay : View {
self
.modifier(ParentViewModifire(presented: presented, anyView: AnyView(overlay)))
}
}
now in your child view you can call this modifier on your view
struct ChildView: View {
#State var newItemPopUp:Bool = false
var body: some View {
ZStack{
Text("hello")
.overlayAll(newCardPopup, presented: $newItemPopUp)
}
}
}
App-wide View
If you want it to be app-wide, put in somewhere app-wide! For example, you can add it to the MyProjectApp.swift (or in sceneDelegate for UIKit/AppDelegate projects) file like this:
Note that the button and the State are just for more explanation and you may consider changing them in the way you like
#main
struct SwiftUIAppPlaygroundApp: App { // <- Note that where we are!
#State var showToast = false
var body: some Scene {
WindowGroup {
Button("App-Wide Button") { showToast.toggle() }
ZStack {
ContentView() // <- The app flow
if showToast {
MyCustomToastView().ignoresSafeArea(.all, edges: .all) // <- App-wide overlays
}
}
}
}
}
See? now you can add any sort of view on anywhere of the screen, without blocking animations. Just convert that #State to some sort of AppState like Observables or Environments and boom! 💥 you did it!
Note that it is a demo, you should use an environment variable or smt to be able for changing it from outside of this view's body
Apple does not currently provide any APIs that allow you to make global views similar to their own alert pop-ups.
In fact these views are actually still using UIKit under the hood.
If you want your own global pop-ups you can sort of hack your own (note this isn't tested, but something very similar should work for global presentation of toasts):
import SwiftUI
import Foundation
/// Global class that will manage toasts
class ToastPresenter: ObservableObject {
// This static property probably isn't even needed as you can inject via #EnvironmentObject
static let shared: ToastPresenter = ToastPresenter()
private init() {}
#Published private(set) var isPresented: Bool = false
private(set) var text: String?
private var timer: Timer?
/// Call this function to present toasts
func presentToast(text: String, duration: TimeInterval = 5) {
// reset the toast if one is currently being presented.
isPresented = false
self.text = nil
timer?.invalidate()
self.text = text
isPresented = true
timer = Timer(timeInterval: duration, repeats: false) { [weak self] _ in
self?.isPresented = false
}
}
}
/// The UI for a toast
struct Toast: View {
var text: String
var body: some View {
Text(text)
.padding()
.background(Capsule().fill(Color.gray))
.shadow(radius: 6)
.transition(AnyTransition.opacity.animation(.default))
}
}
extension View {
/// ViewModifier that will present a toast when its binding changes
#ViewBuilder func toast(presented: Binding<Bool>, text: String) -> some View {
ZStack {
self
if presented.wrappedValue {
Toast(text: text)
}
}
.ignoresSafeArea(.all, edges: .all)
}
}
/// The first view in your app's view hierarchy
struct RootView: View {
#StateObject var toastPresenter = ToastPresenter.shared
var body: some View {
MyAppMainView()
.toast(presented: $toastPresenter.isPresented, text: toastPresenter.text)
// Inject the toast presenter into the view hierarchy
.environmentObject(toastPresenter)
}
}
/// Some view later on in the app
struct SomeViewDeepInTheHierarchy: View {
#EnvironmentObject var toastPresenter: ToastPresenter
var body: some View {
Button {
toastPresenter.presentToast(text: "Hello World")
} label: {
Text("Show Toast")
}
}
}
Use .presentation() to show an alert when the button is tapped.
In LikeButton:
#Binding var liked: Bool
var body: some View {
Button(action: {self.liked = !self.liked}, label: {
Image(systemName: liked ? "heart.fill" : "heart")
}).presentation($liked) { () -> Alert in
Alert.init(title: Text("Thanks for liking!"))
}
}
You can also use .presentation() to present other Modal views, like a Popover or ActionSheet. See here and the "See Also" section on that page in Apple's SwiftUI documentation for info on the different .presentation() options.
Edit: Example of what you want with a custom view using Popover:
#State var liked = false
let popover = Popover(content: Text("Thanks for liking!").frame(width: 200, height: 100).background(Color.white), dismissHandler: {})
var body: some View {
Button(action: {self.liked = !self.liked}, label: {
Image(systemName: liked ? "heart.fill" : "heart")
}).presentation(liked ? popover : nil)
}
I am using this open source: https://github.com/huynguyencong/ToastSwiftUI . It is very simple to use.
struct ContentView: View {
#State private var isShowingToast = false
var body: some View {
VStack(spacing: 20) {
Button("Show toast") {
self.isShowingToast = true
}
Spacer()
}
.padding()
// Just add a modifier to show a toast, with binding variable to control
.toast(isPresenting: $isShowingToast, dismissType: .after(3)) {
ToastView(message: "Hello world!", icon: .info)
}
}
}