I have a textfield on a form where user can type value, but I would also like to update the content of the textfield with a button.
Here is my code :
struct test: View {
#State private var amount: Double = 0.0
var body: some View {
Form {
VStack {
HStack {
Text("Amount EUR")
Spacer()
TextField("Type amount", value: $amount, format: .number)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
}
Text("Set MAX (999)")
.frame(maxWidth: .infinity, alignment: .leading)
.onTapGesture {
print("before tap \(amount )")
amount = 999
print("after tap \(amount)")
}
}
}
}
When I just launch the app, the first tap on the Text updates the textfield with 999, but after it does not work anymore.
The amount value is correctly updated but the textfield does not reflect the change.
Would you have an explanation ?
The answer is simply that TextFields don't update while they are in focus. To solve this problem, you need to incorporate a #FocusState in to the view, and cause the TextField to lose focus right before updating the variable. You can test it in your own view by tapping your Text prior to tapping in to the TextField. You will see it updates just fine.
struct ButtonUpdateTextField: View {
#State private var amount: Double = 0.0
#FocusState var isFocused: Bool // <-- add here
var body: some View {
Form {
VStack {
HStack {
Text("Amount EUR")
Spacer()
TextField("Type amount", value: $amount, format: .number)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
.focused($isFocused) // <-- add here
}
Text("Set MAX (999)")
.frame(maxWidth: .infinity, alignment: .leading)
.onTapGesture {
print("before tap \(amount )")
isFocused = false // <-- add here
amount = 999
print("after tap \(amount)")
}
}
}
}
}
Related
I have a fixed height ScrollView containing a Text() which is constantly being updated from my viewModel.
If the text is too much to be viewed all at once, i.e. I need to scroll to see the end of the text, I’d like it to be automatically scrolled so that I always see the end of the text.
Is that possible?
ScrollView {
Text(vm.text)
.frame(minWidth: 20, alignment: .leading)
}
.frame(height: 200)
Note: this is a very simplified version of my problem. In my app there are times when the text is not being updated and it does need to be scrollable.
I have tried scrollViewReader … something like:
ScrollView {
ScrollViewReader() { proxy in
Text(vm.text)
.frame(minWidth: 20, alignment: .leading)
Text("").id(0)
}
}
.frame(height: 200)
with the idea of scrolling to the empty Text, but I couldn’t work out how to trigger
withAnimation {
proxy.scrollTo(0)
}
... all the examples I've seen use a button but I need to trigger when the text updates.
You can use .onChange to react to change of vm.text and then scroll to the end.
Please note that ScrollView should be inside the ScrollViewReader!
In principle it would work like this:
struct ContentView: View {
#State private var vmtext = "Test Text"
// timer change of text for testing
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Text(vmtext)
.id(0)
}
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
proxy.scrollTo(0, anchor: .bottom)
}
}
.frame(width: 100, height: 200, alignment: .leading)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
vmtext += " added new text"
}
}
}
The problem a have here is that it only works after the scrollview has been scrolled manually once.
I've adapted the code by #ChrisR with a hack that at the very least, shows how i'd like scrollTo: to work. It toggles between 2 almost identical views with different ids (one has a tiny bit of padding)
import SwiftUI
struct ContentView: View {
#State private var vmtext = "\n\n\n\nTest Text"
#State private var number = 0
#State private var id = 0
// timer change of text for testing
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
id == 0 ?
VStack {
Text(vmtext)
.padding(.top, 0)
.font(.title2)
.id(0)
}
:
VStack {
Text(vmtext)
.padding(.top, 1)
.font(.title2)
.id(1)
}
}
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
print("onChange entered. id: \(id)")
proxy.scrollTo(id, anchor: .bottom)
}
}
.frame(height: 90, alignment: .center)
.frame(maxWidth: .infinity)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
if id == 0 { id = 1} else {id = 0}
number += 1
vmtext += " word\(number)"
}
}
}
Notes:
It seems that if the height of the text view being shown hasn't changed the proxy.scrollTo is ignored. Set the paddings the same and the hack breaks.
Removing the "\n\n\n\n" from the var vmtext breaks the hack. They make the initial size of the text view bigger than the scrollview window and so immediately scrollable - or something :-). If you do remove them, scrolling will start working after you do an initial scroll with your finger.
EDIT:
Here is a version without the padding and "\n\n\n\n" hacks, which uses a double rotation hack.
import SwiftUI
struct ContentView: View {
#State private var vmtext = "Test Text"
#State private var number = 0
#State private var id = 0
// timer change of text for testing
let timer = Timer.publish(every: 0.3, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Group {
id == 0 ?
VStack {
Text(vmtext)
.font(.title2)
.id(0)
}
:
VStack {
Text(vmtext)
.font(.title2)
.id(1)
}
}
.padding()
.rotationEffect(Angle(degrees: 180))
}
.rotationEffect(Angle(degrees: 180))
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
withAnimation {
if id == 0 { id = 1 } else { id = 0 }
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame(height: 180, alignment: .center)
.frame(maxWidth: .infinity)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
number += 1
vmtext += " word\(number)"
}
}
}
It would be nice if the text started at the top of the view and only scrolls when the text has filled up the view, as with the swiftui TextEditor ... and the withAnimation doesn't work.
I'm trying to make navigation link, here I'm creating NavigationLink with isActive based on State variable isLoggedIn. But without setting isLoggedIn true getting navigating to next screen.
also, it's navigating on tap of Email Textfield which is wrong.
My expectation is it should navigate only after isLoggedIn setting to true.
struct ContentView: View {
#State private var isLoggedIn = false
#State private var email = ""
var body: some View {
NavigationView {
NavigationLink(destination: Text("Second View"), isActive: $isLoggedIn) {
VStack {
TextField("Email", text: $email)
.frame(maxWidth: .infinity, alignment: .leading)
.border(.gray, width: 1)
.foregroundColor(.blue)
Button("Send") {
isLoggedIn = true
}
}
.padding()
}
}
}
}
The expectation is wrong, NavigationLink handles user input independently (but also, additionally, can be activated programmatically).
In this scenario, to leave only programmatic activation, we need to hide navigation link, like
NavigationView {
VStack {
TextField("Email", text: $email)
.frame(maxWidth: .infinity, alignment: .leading)
.border(.gray, width: 1)
.foregroundColor(.blue)
Button("Send") {
isLoggedIn = true
}
.background(NavigationLink(destination: // << here !!
Text("Second View"), isActive: $isLoggedIn) { EmptyView() })
}
.padding()
}
Here it's working fine with this
struct MoviesListView: View {
#State var navigate = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Hi"), isActive: $navigate) {
Button("Add") {
navigate.toggle()
}
}
}
}
}
}
As long as the datePicker selection is closed, everything is fine. When the calendar sheet is left open, and the dismiss (.onTapGesture) is pressed, the onDismiss closure is done, but the screen will not return. Tried setting the isPresenting to false in the didDismiss() with the same results.
getting this error
[Presentation] Attempt to present <TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView: 0x116808680> on <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_: 0x116a09230> (from <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_: 0x116a09230>) which is already presenting <TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView: 0x1168043a0>.
Took this code from the documentation and added a DatePicker to make things a simple a possible.
struct FullScreenCoverPresentedOnDismiss: View {
#State private var isPresenting = false
#State private var birthDate = Date()
var body: some View {
Button("Present Full-Screen Cover") {
isPresenting.toggle()
}
.fullScreenCover(isPresented: $isPresenting,
onDismiss: didDismiss) {
VStack {
DatePicker(selection: $birthDate, in: ...Date(), displayedComponents: .date) {
Text("Select a date")
}
Text("A full-screen modal view.")
.font(.title)
Text("Tap to Dismiss")
.onTapGesture {
isPresenting.toggle()
}
}
.foregroundColor(.white)
.frame(maxWidth: .infinity,
maxHeight: .infinity)
.background(Color.blue)
.ignoresSafeArea(edges: .all)
}
}
func didDismiss() {
print("didDismiss")
}
}
so I am trying to have a view update to display a custom view based on a user selection from another view. This is a simple task app project I started to get a better understanding of SwiftUI and have hit my first major roadblock. The custom view is generated from a Tag object from Core Data, so it would be this information that is passed from View 2 to View 1.
I've marked where the update would take place as well as where the action is performed with TODOs. Hopefully I did a good job at explaining what I am hoping to accomplish, nothing I have tried seems to work. I am sure it's something simple but the solution is evading me.
View 1: View that needs to be updated when user returns
View 2: View where selection is made
The View that needs to be updated and its ViewModel.
struct AddTaskView: View {
//MARK: Variables
#Environment(\.managedObjectContext) var coreDataHandler
#Environment(\.presentationMode) var presentationMode
#StateObject var viewModel = AddTaskViewModel()
#StateObject var taskListViewModel = TaskListViewModel()
#State private var title: String = ""
#State private var info: String = ""
#State private var dueDate = Date()
var screenWidth = UIScreen.main.bounds.size.width
var screenHeight = UIScreen.main.bounds.size.height
var body: some View {
VStack(spacing: 20) {
Text("Add a New Task")
.font(.title)
.fontWeight(.bold)
//MARK: Task.title Field
TextField("Task", text: $title)
.font(.headline)
.padding(.leading)
.frame(height: 55)
//TODO: Update to a specific color
.background(Color(red: 0.9, green: 0.9, blue: 0.9))
.cornerRadius(10)
//MARK: Task.tag Field
HStack {
Text("Tag")
Spacer()
//TODO: UPDATE TO DISPLAY TAG IF SELECTED OTHERWISE DISPLAY ADDTAGBUTTONVIEW
NavigationLink(
destination: TagListView(),
label: {
AddTagButtonView()
}
)
.accentColor(.black)
}
//MARK: Task.info Field
TextEditor(text: $info)
.frame(width: screenWidth - 40, height: screenHeight/4, alignment: .center)
.autocapitalization(.sentences)
.multilineTextAlignment(.leading)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black, lineWidth: 0.5)
)
//MARK: Task.dateDue Field
DatePicker(
"Due Date",
selection: $dueDate,
in: Date()...
)
.accentColor(.black)
.font(.headline)
Spacer()
Button(action: {
viewModel.addTask(taskTitle: title, taskInfo: info, taskDueDate: dueDate)
//Dismiss View if successful
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Add Task")
.frame(width: 150, height: 60)
.font(.headline)
.foregroundColor(.black)
.background(Color.yellow)
.cornerRadius(30)
})
}
.padding()
.navigationBarTitleDisplayMode(.inline)
}
}
final class AddTaskViewModel : ObservableObject {
var coreDataHandler = CoreDataHandler.shared
#Published var tag : Tag?
func addTask(taskTitle: String, taskInfo: String, taskDueDate: Date) {
let newTask = Task(context: coreDataHandler.container.viewContext)
newTask.title = taskTitle
newTask.info = taskInfo
newTask.dateCreated = Date()
newTask.dateDue = taskDueDate
newTask.completed = false
newTask.archived = false
coreDataHandler.save()
}
}
The View where the selection is made and its ViewModel
struct TagListView: View {
#FetchRequest(entity: Tag.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Tag.title, ascending: true)]) var tagList : FetchedResults<Tag>
#Environment(\.presentationMode) var presentationMode
#StateObject var viewModel = TagListViewModel()
var body: some View {
VStack {
HStack {
Text("Create a Tag")
.font(.system(size: 20))
.fontWeight(.medium)
Spacer()
NavigationLink(
destination: CreateTagView(),
label: {
Image(systemName: "plus.circle")
.font(.system(size: 25))
})
}
Divider()
.padding(.bottom, 10)
ScrollView(.vertical, showsIndicators: false, content: {
if tagList.count != 0 {
LazyVStack(spacing: 20) {
ForEach(tagList, id: \.self) { tag in
let tagColour = Color(red: tag.colourR, green: tag.colourG, blue: tag.colourB, opacity: tag.colourA)
Button {
//TODO: UPDATE ADDTASKVIEW TO DISPLAY THE SELECTED TAG
//Dismiss view
self.presentationMode.wrappedValue.dismiss()
} label: {
TagView(title: tag.title ?? "Tag", color: tagColour, darkText: false)
}
}
}
} else {
Text("Add your first tag.")
}
})
}
.padding()
}
}
final class TagListViewModel : ObservableObject {
}
I am presenting a "wizard" that will be detecting a BLE device and then if it is the correct one the last view will ask if we want to register or skip.
Edit:{
the view order is: MainView presenting in fullScreenCover a first info view informing on how to detect the BLE device then this one pushes a second view with some info on the nearest BLE device and it is in this view that we have the fork where I am presenting a sheet to ask if the user wants to continue and register the BLE device or skip.
So MAIN > INFOView -> BLE detection (> Register or skip ? RegisterView : Destack to main)
}
I have that last view come up as a sheet it has 2 buttons, the first one as mentioned says "Register" and the other one says "skip". If the user presses the register then we dismiss the sheet and navigate to a view that is gathering personal info to register the BLE device. on the other hand, if the user chooses to skip then the wizard need to de-stack back over to the main view.
Normally in UIKit I would just have a delegate inform me of the choice then if skip was selected. I would call pop to root view controller, otherwise, if the register option was selected I would dismiss the sheet view and then navigate to one more final view and get the user registered.
In SwiftUI I do not know how to deal with that navigation fork. I tried using PassthroughSubject but then I have to set the PassthroughSubject var as a state var and in the end, I just did not get the call back from sending in the selection.
Tried binding then Was hoping to make an onReceive but then it is asking for a publisher and that felt wrong to create a publisher just for that.
I am wondering g what is the best way do take care of this in. swiftUI ?
edit:
this is the code (updated with the replay from #Predrag Samardzic) for the view that shows the info on the BLE device (smart bike) and will push at first a request to know if the user wants to register or not, then if yes push that registration screen if not dismiss the entire stack.
struct A18BikeDiscoveryView: View {
#EnvironmentObject var bleManager: ArgonBLEManager
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
private let shouldShowRegistration = CurrentValueSubject<Bool, Never>(false)
#State var isSheetPresented = false
#State var isRegistrationPresented = false
var body: some View {
VStack{
NavigationLink(
destination: A18RegistrationQuestionairy(QuestionairyViewModel()),
isActive: $isRegistrationPresented
) {
EmptyView()
}
A18ImageTextBanner(text: NSLocalizedString("bike_discovery_view_title", comment: ""))
.padding(.bottom, 35)
.navigationBarBackButtonHidden(true)
if let value = bleManager.model?.bikeInfo?.bikeModel{
Text(value)
.fontWeight(.bold)
.scaledFont(.largeTitle)
}
Image("subitoBike")
.resizable()
.frame(minWidth: 0334, idealWidth: 334, maxWidth: .infinity, minHeight: 223, idealHeight: 223, maxHeight: .infinity, alignment: .center)
.aspectRatio(contentMode: .fit)
.padding(.bottom, 10)
Divider()
VStack(alignment: .leading){
HStack{
Text("bike_discovery_view_year_created")
if let v = bleManager.model?.bikeInfo?.year{
Text(v)
}
}
HStack{
Text("bike_discovery_view_model_size")
Text("\(getSizeFromSerial())")
}
HStack{
Text("bike_discovery_view_bike_serial_number")
if let v = bleManager.model?.bikeInfo?.bikeSerialNumber {
Text(v)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 66, alignment: .leading)
.padding(.horizontal, 40)
Divider()
.padding(.bottom, 30)
Button(action: {
isSheetPresented = true
}, label: {
Text("bike_discovery_view_bike_pairing_button_title")
.fontWeight(.bold)
.foregroundColor(.white)
})
.buttonStyle(A18RoundButtonStyle(bgColor: .red))
.padding(.horizontal)
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
if shouldShowRegistration.value {
isRegistrationPresented = true
}},
content: {
A18BikeParingSelection(shouldShowRegistration: shouldShowRegistration)
})
.onReceive(shouldShowRegistration) { shouldShowRegistration in
isSheetPresented = false
}
Button(action: {
bleManager.disconect()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("bike_discovery_view_bike_pairing_cancel_button_title")
.fontWeight(.bold)
.foregroundColor(Color("grey55"))
})
.padding()
Spacer()
}
.navigationBarColor(backgroundColor: .white, tintColor: .black)
.navigationBarTitleDisplayMode(.inline)
}
func getSizeFromSerial() -> String {
if let serial = bleManager.model?.bikeInfo?.bikeSerialNumber {
if serial.contains("XXS"){
return "XXS"
}else if serial.contains("XSM") {
return "XS"
}else if serial.contains("SML"){
return "S"
}else if serial.contains("MED"){
return "M"
}else if serial.contains("LAR"){
return "L"
}
}
return "N/A"
}
}
This is one possible solution - using CurrentValueSubject in order to trigger dismiss and keep info about the choice made on the presented screen. Then, if registration is needed, you trigger it when sheet is dismissed.
struct MainView: View {
private let shouldShowRegistration = CurrentValueSubject<Bool, Never>(false)
#State var isSheetPresented = false
#State var isRegistrationPresented = false
var body: some View {
VStack {
// this part is if you want to push registration screen, you will need to have MainView inside NavigationView for it
NavigationLink(
destination: RegistrationView(),
isActive: $isRegistrationPresented
) {
EmptyView()
}
// ----------------------------------------------------
Button {
isSheetPresented = true
} label: {
Text("Present sheet")
}
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
if shouldShowRegistration.value {
isRegistrationPresented = true
}},
content: {
ChoiceView(shouldShowRegistration: shouldShowRegistration)
})
.onReceive(shouldShowRegistration) { shouldShowRegistration in
isSheetPresented = false
}
// this part is if you want to present registration screen as sheet
// .sheet(
// isPresented: $isRegistrationPresented,
// content: {
// RegistrationView()
// })
}
}
}
struct ChoiceView: View {
let shouldShowRegistration: CurrentValueSubject<Bool, Never>
var body: some View {
VStack{
Button {
shouldShowRegistration.send(false)
} label: {
Text("Dismiss")
}
Button {
shouldShowRegistration.send(true)
} label: {
Text("Register")
}
}
}
}
struct RegistrationView: View {
var body: some View {
Text("Registration")
}
}