Contracting an expanded DatePicker on SwiftUI / MacOS - swift

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.

Related

SwiftUI: Why do I get the same pattern of random items in an array?

I'm working on my project and there's a section where you can test yourself on Japanese letters. You tap a button that plays a sound of a letter then you choose the right button out of three with the correct letter. there are 10 questions in total and it should always randomize, the letters but whenever you come back to the view, the first question is always the same. after the first one it randomizes the rest of the questions but the first question always has the same pattern of letters. What I want is getting a random pattern of letters for the first question every time you come back to the view. I'd appreciate any help.
here's the code of QuestionView:
import SwiftUI
struct HiraganaQuiz: View {
var hiraganas: [Japanese] = Bundle.main.decode("Hiragana.json")
#State private var correctAnswer = Int.random(in: 0...45)
#StateObject var soundplayer = Audio()
#State private var answer = ""
#State private var counter = 0
#State private var correctAnswerCounter = 0
#State private var showingAlert = false
#State private var alertMessage = ""
#State private var disabled = false
var body: some View {
ZStack {
Color.darkBackground
.ignoresSafeArea()
VStack {
Text("\(counter) / 10")
.padding(.top,40)
.foregroundColor(.white)
.font(.system(size:30).bold())
Text("Tap the speaker and choose the right letter")
Button {
soundplayer.playSounds(file: hiraganas[correctAnswer].voice1)
} label: {
Image(systemName: "speaker.wave.3.fill")
}
.font(.system(size:70))
height: 110)
HStack {
ForEach (0...2, id: \.self) { index in
Button {
letterTapped(index)
} label: {
Text(hiraganas[index].letter)
}
}
.disabled(disabe())
.foregroundColor(.white)
.font(.system(size: 35).bold())
Text("\(answer)")
.foregroundColor(.white)
.padding(.bottom,20)
.font(.system(size: 30))
Button {
resetTheGame()
} label: {
Text("Next")
}.buttonStyle(.plain)
.font(.system(size: 30).bold())
.frame(width: 200, height: 50)
}
}
.alert("⭐️ Well done ⭐️", isPresented: $showingAlert) {
Button("Retry", action: reset)
} message: {
Text(alertMessage)
}
} .onAppear { hiraganas = Bundle.main.decode("Hiragana.json").shuffled() }
}
I'm surprised this even compiles -- it should be giving you an error about trying to initialize state like this.
Instead, you can shuffle in the properties in the property initializer:
struct HiraganaQuiz: View {
#State private var hiraganas: [Japanese] = Bundle.main.decode("Hiragana.json").shuffled()
Update, to show the request for onAppear usage:
struct HiraganaQuiz: View {
#State private var hiraganas: [Japanese] = []
var body: some View {
ZStack {
// body content
}.onAppear {
hiraganas = Bundle.main.decode("Hiragana.json").shuffled()
}
}
}

PopUp don't want to dismiss SwiftUI

I am having a problem while I want to dismiss a popup (that appears automatically depending on a specific condition) by clicking a button.
This is the PopUp struct:
struct dataPrivacyPopUp: View {
let model: OffersView.Model
let termsOfUseText = "Nutzungsbedingungen"
let privacyPolicyText = "Datenschutzerklärung"
#State var termsOfUseChecked = false
#State var privacyPolicyChecked = false
#State var buttonDisabled = true
#State private var showPopUp: Bool = false
#Binding var showModal: Bool
var body: some View {
ZStack {
if ( model.showPopUp == true) {
// PopUp Window
VStack(alignment: .center){
Image("logo")
.aspectRatio(contentMode: .fill)
.frame(alignment: .center)
.padding()
VStack(alignment: .leading) {
Text((model.acceptance?.salutation)!)
.multilineTextAlignment(.leading)
.padding()
.foregroundColor(Color.black)
Text((model.acceptance?.statement)!)
.multilineTextAlignment(.leading)
.padding()
.foregroundColor(Color.black)
Text((model.acceptance?.declarationIntro)!)
.multilineTextAlignment(.leading)
.padding()
.foregroundColor(Color.black)
if ((model.acceptance?.dpr)! == true) {
VStack(alignment: .leading){
HStack {
CheckBoxView(checked: $privacyPolicyChecked)
HStack(spacing: 0){
Text(R.string.localizable.dataPrivacyPopupText())
.foregroundColor(Color.black)
Button(privacyPolicyText) {
model.openUrl(url: API.privacyPolicyURL)
}
}
}
Text((model.acceptance?.declarationOutro)!)
.multilineTextAlignment(.leading)
.padding()
}
.padding()
Button(action: {
model.setTos()
print("showModal PopUpView2 1: \(showModal)")
self.showModal.toggle()
print("showModal PopUpView2 2: \(showModal)")
}, label: {
Text(R.string.localizable.dataPrivacyButton())
.foregroundColor(Color.white)
.font(Font.system(size: 23, weight: .semibold))
})
.disabled(model.buttonDisabledForOne(privacyPolicyChecked: privacyPolicyChecked, termsOfUseChecked: termsOfUseChecked))
.padding()
}
}
}
// .onAppear(perform: )
.background(Color.white01)
.padding()
}
}
}
}
and this is where I call it (contentView):
struct OffersView: View {
#StateObject var model = Model()
#State private var showingPopUp = false
#State private var showModal = false
#State private var showingAddUser = false
// var showPopup : Bool = true
var body: some View {
NavigationView {
Group {
switch model.sections {
case .loading:
ActivityIndicator(animate: true)
case .success(let sections):
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 0) {
Text(R.string.localizable.offersHello(model.firstName))
.aplFont(.headline02)
.padding(.bottom, 24)
VStack(spacing: 48) {
ForEach(sections) { section in
OffersSectionView(section: section, model: model)
}
}
}
.useFullWidth(alignment: .leading)
.padding()
}
default:
Color.clear
if ( model.showPopUp == true) {
ZStack {
Color.black.opacity(model.showPopUp ? 0.3 : 0).edgesIgnoringSafeArea(.all)
dataPrivacyPopUp(model: model, showModal: self.$showModal)
.onAppear(perform: {
self.showModal.toggle()
})
}
}
}
}
.navigationBarHidden(true)
.handleNavigation(model.navigationPublisher)
.onAppear(perform: model.onAppear)
.onDisappear(perform: model.onDisappear)
.environment(\.dynamicTypeEnabled, false)
.safariView(isPresented: model.showSafari) {
SafariView(url: model.safariUrl!)
}
}
}
}
I need help about this, I tried the traditional method to set a #Binding variable etc .. but that's not working, the boolean value is changing but the UI is not updating (the popup is not dismissing), thank you
I tried to look at your code - I suggest you simplify it to the bare minimum to exemplify your issue - and it seems that you are using 2 properties to show your pop-up: showingPopUp and showModal. It is quite likely that you are having trouble keeping them both in sync.
For starters, I would suggest to use only one variable, either it is true or false - "a man with two watches never knows what time it is".
For the solution:
If you prefer keeping your ZStack approach, the solution would look something like:
struct MyPrivacy: View {
#Binding var showMe: Bool
var body: some View {
VStack {
Text("The content of the pop-up")
.padding()
Button {
withAnimation {
showMe.toggle()
}
} label: {
Text("Dismiss")
}
}
}
}
struct Offers: View {
#State private var showPopup = false
var body: some View {
NavigationView {
ZStack {
VStack {
Text("View behind the pop-up")
.padding()
Button {
withAnimation {
showPopup.toggle()
}
} label: {
Text("Pop")
}
}
if showPopup {
Color.white
MyPrivacy(showMe: $showPopup)
}
}
}
}
}
If instead you want to go for a more flexible approach, if you are developing for iOS, SwiftUI has a convenient object - Sheets. You can use it as suggested in the documentation, or build a specific struct that manages all the modal views of this type and use your model to handle the presentation.
The process goes like:
Create a struct that will handle all kinds of Sheets of your app.
Add to your view-model the property to present any sheet.
Create the Views that will be the content of each sheet.
Call the .sheet(item:content:) method on each View the requires a sheet.
Here's the sample code:
SheetView handler:
struct SheetView: Identifiable {
// This struct controls what modal view will be presented.
// The enum SheetScreenType can grow to as many as different
// modal views your app needs - add the content in the switch below.
let id = UUID()
var screen: SheetScreenType
#ViewBuilder
var content: some View {
switch screen {
case .dataPrivacy:
DataPrivacy()
default:
EmptyView()
}
}
enum SheetScreenType {
case dataPrivacy
case none
}
}
Presenter in your view-model:
class MyViewModel: ObservableObject {
// This code can fit anywhere within your view-model.
// It controls the presentation of the modal view, which in
// this case is a Sheet.
private let sharedSheet = SheetView(screen: .none)
// Show the selected sheet
#Published var sheetView: SheetView?
var showSheet: SheetView.SheetScreenType {
get {
return sheetView?.screen ?? .none
}
set {
switch newValue {
case .none:
sheetView = nil
default:
sheetView = sharedSheet
}
sheetView?.screen = newValue
}
}
}
Content of your modal view:
struct DataPrivacy: View {
#EnvironmentObject var model: MyViewModel // Pass YOUR model here
var body: some View {
VStack(alignment: .center){
Text("Respecting your privacy, no details are shown here")
.padding()
Button {
print("Anything you need")
// Set the showSheet property of your model to
// present a modal view. Setting it to .none dismisses
// the modal view.
model.showSheet = .none
} label: {
Text("Time do dismiss the modal view")
}
.padding()
}
}
}
Enable your view to listen to your model to present the sheet:
struct OffersView: View {
#ObservedObject var model = MyViewModel() // Pass YOUR model here
var body: some View {
VStack {
Text("Anything you wish")
.padding()
Button {
withAnimation {
// Set the showSheet property of your model to
// present a modal view. Set it to any choice
// among the ones in the SheetScreen.SheetScreenType enum.
model.showSheet = .dataPrivacy
}
} label: {
Text("Tap here for the privacy in modal view")
}
}
// Show a modal sheet.
// Add this property at the top level of every view that
// requires a modal view presented - whatever content it might have.
.sheet(item: $model.sheetView) { sheet in
sheet.content
.environmentObject(model)
}
}
}
Good luck with your project!

Problem with SwiftUI DatePicker animation

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

TextField with animation crashes app and looses focus

Minimal reproducible example:
In SceneDelegate.swift:
let contentView = Container()
In ContentView.swift:
struct SwiftUIView: View {
#State var text: String = ""
var body: some View {
VStack {
CustomTextFieldView(text: $text)
}
}
}
struct Container: View {
#State var bool: Bool = false
#State var text: String = ""
var body: some View {
Button(action: {
self.bool.toggle()
}) {
Text("Sheet!")
}
.sheet(isPresented: $bool) {
SwiftUIView()
}
}
}
In CustomTextField.swift:
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing: Bool = false
var body: some View {
Group {
if self.editing {
textField
.background(Color.red)
} else {
ZStack(alignment: .leading) {
textField
.background(Color.green)
Text("Placeholder")
}
}
}
.onTapGesture {
withAnimation {
self.editing = true
}
}
}
var textField: some View {
TextField("", text: $text)
}
Problem:
After running the above code and focusing the text field, the app crashes. Some things I noticed:
If I remove the withAnimation code, or the ZStack in CustomTextField file, the app doesn't crash, but the TextField looses focus.
If I remove the VStack in SwiftUIView, the app doesn't crash, but the TextField looses focus.
If I use a NavigationLink or present the TextField without a sheet, the app doesn't crash, but the TextField looses focus.
Questions:
Is this a problem in the current version of SwiftUI?
Is there a solution to this problem using SwiftUI? I want to stay out of
ViewRepresentables as much as possible.
How can I keep the focus of the TextField after the body is recalculated because of a change in state?
How can I keep the focus of the TextField after the body is
recalculated because of a change in state?
You have two of them. Two different TextField could not be in editing state at the same time.
The approach suggested by Asperi is the only possible.
The reason, why your code crash is not easy explain, but expected in current SwiftUI.
You have to understand, that Group is not a standard container, it just like a "block" on which you can apply some modifiers. Removing Group and using wraping body in ViewBuilder
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing: Bool = false
#ViewBuilder
var body: some View {
if self.editing {
TextField("", text: $text)
.background(Color.red)
.onTapGesture {
self.editing.toggle()
}
} else {
ZStack(alignment: .leading) {
TextField("", text: $text)
.background(Color.green)
.onTapGesture {
self.editing.toggle()
}
}
}
}
}
the code will stop to crash, but there is other issue, the keyboard will dismiss immediately. That is due the tap gesture applied.
So, believe or not, you have to use ONE TextField ONLY.
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing = false
var textField: some View {
TextField("", text: $text, onEditingChanged: { edit in
self.editing = edit
})
}
var body: some View {
textField.background(editing ? Color.green : Color.red)
}
}
Use this custom text field elsewhere in your code, as you want
Try the following for CustomTextFieldView (tested & works with Xcode 11.2 / iOS 13.2)
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing: Bool = false
var body: some View {
TextField("", text: $text)
.background(self.editing ? Color.red : Color.green)
.onTapGesture {
withAnimation {
self.editing = true
}
}
}
}
How can I keep the focus of the TextField after the body is recalculated because of a change in state?
You don't loose focus, you just remove entire text field, so the solution is not replace text field, but modify its property, ie background. It's ok to put it into ZStack, but keep it one.

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