Why does this swiftui view not update? - ios

Consider the following code block:
import SwiftUI
struct MeasurementReading: View, Equatable {
#ObservedObject var ble: BluetoothConnectionmanager
#GestureState var isDetectTap = false
#State var MyText:String = "Wait"
static func == (lhs: MeasurementReading, rhs: MeasurementReading)->Bool{
return lhs.MyText == rhs.MyText
}
var body: some View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
return HStack {
Spacer()
VStack{
Button(action:{
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText as NSString)")
}, label: {
Text(MyText)
.font(.system(size: 40))
.bold()
.foregroundColor(Color.black)
.padding(.trailing, 15)
.frame(height: 100)
})
Button(action: {
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText as NSString)")
}, label: {
Text(MyText)
.font(.system(size: 25))
.padding(.top, -20)
.padding(.bottom, 20)
.foregroundColor(Color.black)
})
}
}.onReceive(timer)
{ _ in // TIMER FUNCTIONALITY HERE
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText)")
}
}
}
struct MeasurementReading_Previews: PreviewProvider {
static var previews: some View {
MeasurementReading(ble: BluetoothConnectionmanager())
}
}
Every 1 second the correct value read from the BLE system is assigned to MyText and then MyText is printed to the debug output properly with the updated value.
The problem here is that view MeasurementReading does not update. Also, using a closure on any item also has the same behavior (variable is updated, it is output properly but no view update) ex .onTap{....} will have the same behavior or any other .onXXXX closure. The only way I could get the view to update at all with new values for the MyText state is to put the behavior in a Button.
My question is this: Why does the view not update even when the state variable changes via Timer or .onXXXX closure?

You need to be setting the ble value to the updated timer value:
Without testing this properly. I also think your BluetoothConnectionmanager needs to be a #State property for this to work.
#State var ble: BluetoothConnectionmanager
.onReceive(timer) { value in // value is the updated value
self.ble.value = value
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText)")
}
Take a look at this example to see how a timer works with a Date() object.

Related

Swift - Updating Binding<String> stored value programmatically from View extension

So my goal is to have a more convenient method for adding a placeholder text value on SwiftUI's TextEditor, since there doesn't appear to be one. The approach I'm trying has uncovered something I really don't understand around Binding<> wrapped types. (Maybe this is a red flag that I'm doing something not recommended?)
Anyway, on to my question: are we able to programmatically update the underlying values on Bindings? If I accept some Binding<String> value, can I update it from within my method here? If so, will the updated value be referenced by the #State originator? The below example places my placeholder value in as text where I'm trying to type when you click into it, and does not even attempt it again if I clear it out.
Imported this code from other posts I found some time ago to make it display a placeholder if the body is empty.
import Foundation
import SwiftUI
struct TextEditorViewThing: View {
#State private var noteText = ""
var body: some View {
VStack{
TextEditor(text: $noteText)
.textPlaceholder(placeholder: "PLACEHOLDER", text: $noteText)
.padding()
}
}
}
extension TextEditor {
#ViewBuilder func textPlaceholder(placeholder: String, text: Binding<String>) -> some View {
self.onAppear {
// remove the placeholder text when keyboard appears
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if text.wrappedValue == placeholder {
text.wrappedValue = placeholder
}
}
}
// put back the placeholder text if the user dismisses the keyboard without adding any text
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if text.wrappedValue == "" {
text.wrappedValue = placeholder
}
}
}
}
}
}
Customize this setup as per your requirement:
struct ContentView: View {
#State private var text: String = ""
var body: some View {
VStack {
ZStack(alignment: .leading) {
if self.text.isEmpty {
VStack {
Text("Placeholder Text")
.multilineTextAlignment(.leading)
.padding(.leading, 25)
.padding(.top, 8)
.opacity(0.5)
Spacer()
}
}
TextEditor(text: $text)
.padding(.leading, 20)
.opacity(self.text.isEmpty ? 0.5 : 1)
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height/2)
.overlay(
Rectangle().stroke()
.foregroundColor(Color.black)
.padding(.horizontal, 15)
)
}
}
}

How do you send values to other classes using ObservableObject in SwiftUI

Im trying to create a simple tip Calculator but i am having a problem once a user has added in the information in the app instead of calculating the tip, the function is returning as if there is an issue somewhere.
Im trying to make it so that when a user inputs a number in the text field and selects a percentage the total tip is displayed underneath.
How do i fix this problem?
Ive added a print statement to print the word "returning" and it keeps printing this word so i think the problem is somewhere in the calculateTip function:
This is my Code for my ContentView class:
struct ContentView: View {
//MARK: - PROPERTIES
#ObservedObject public var tipVM = TipViewModel()
//MARK: - FUNCTIONS
private func endEditing() {
hideKeyboard()
}
//MARK: - BODY
var body: some View {
Background {
NavigationView {
ZStack {
VStack {
HStack(spacing: 10) {
Text("Tip Calculator")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.heavy)
.padding(.leading, 4)
.foregroundColor(.blue)
Spacer()
Button(action: {
// tipVM.clearFields()
}, label: {
Text("Clear")
.font(.system(size: 16, weight: .semibold, design: .rounded))
.padding(.horizontal, 10)
.frame(minWidth: 70, minHeight: 24)
.background(
Capsule().stroke(lineWidth: 2)
)
.foregroundColor(.blue)
}) //: BUTTON
} //: HSTACK
.padding()
Spacer(minLength: 80)
TextField("Enter Amount: ", text: $tipVM.amount)
.padding()
.background(Color.secondary)
.foregroundColor(.white)
.font(.system(.title3, design: .rounded))
Picker(selection: $tipVM.tipPercentage, label: Text("Picker"), content: {
ForEach(0 ..< tipVM.tipChoices.count) { index in
Text("\(self.tipVM.tipChoices[index])%").tag(index)
.font(.system(.body, design: .rounded)).padding()
}.padding()
.background(subtitleColor)
.foregroundColor(.white)
}).onTapGesture(perform: {
tipVM.calculateTip()
})
.pickerStyle(SegmentedPickerStyle())
Text(tipVM.tip == nil ? "£0" : "\(tipVM.tip!)")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.bold)
.foregroundColor(.blue)
.padding()
} //: VSTACK
} //: ZSTACK
.navigationBarHidden(true)
} //: NAVIGATION VIEW
.navigationViewStyle(StackNavigationViewStyle())
}.onTapGesture {
self.endEditing()
}
.ignoresSafeArea(.keyboard, edges: .all)
} //: BACKGROUND
}
And here is my code for my TipViewModel Class:
import Foundation
import SwiftUI
import Combine
class TipViewModel: ObservableObject {
var amount: String = ""
var tipPercentage: Int = 0
var tip: Double?
let tipChoices = [10,15,20,25,30]
let didChange = PassthroughSubject<TipViewModel, Never>()
func calculateTip() {
guard let amount = Double(amount) else {
print("returning")
return
}
self.tip = amount * (Double(tipPercentage)/100)
self.didChange.send(self)
}
}
I would appreciate any help thanks.
The usual way is to mark the properties with #Published whose changes are going to be monitored. The extra Combine subject is not needed.
And declare tip as non-optional
import Foundation
import SwiftUI
// import Combine
class TipViewModel: ObservableObject {
var amount: String = ""
var tipPercentage: Int = 0
#Published var tip = 0.0
let tipChoices = [10,15,20,25,30]
func calculateTip() {
guard let numAmount = Double(amount) else {
print("returning")
return
}
self.tip = numAmount * (Double(tipPercentage)/100)
}
}
Secondly if the current view struct creates (aka owns) the observable object use always #StateObject rather than #ObservedObject. The latter is for objects which are initialized at higher levels in the view hierarchy and just passed through.
struct ContentView: View {
//MARK: - PROPERTIES
#StatedObject private var tipVM = TipViewModel()
...
Text("£\(tipVM.tip)")

How to make a button (or any other element) show SwiftUI's DatePicker popup on tap?

I'm trying to achieve the simplest possible use case, but I can't figure it out.
I have a picture of calendar. All I want is to show DatePicker popup when tapping the picture.
I tried to put it inside ZStack, but by doing it I can't hide default data textfields:
ZStack {
Image("icon-calendar")
.zIndex(1)
DatePicker("", selection: $date)
.zIndex(2)
}
How to make this simple layout natively without ridiculous workarounds?
I'm having this problem too. I couldn't sleep for a few days thinking about the solution. I have googled hundred times and finally, I found a way to achieve this. It's 1:50 AM in my timezone, I can sleep happily now. Credit goes to chase's answer here
Demo here: https://media.giphy.com/media/2ILs7PZbdriaTsxU0s/giphy.gif
The code that does the magic
struct ContentView: View {
#State var date = Date()
var body: some View {
ZStack {
DatePicker("label", selection: $date, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.labelsHidden()
Image(systemName: "calendar")
.resizable()
.frame(width: 32, height: 32, alignment: .center)
.userInteractionDisabled()
}
}
}
struct NoHitTesting: ViewModifier {
func body(content: Content) -> some View {
SwiftUIWrapper { content }.allowsHitTesting(false)
}
}
extension View {
func userInteractionDisabled() -> some View {
self.modifier(NoHitTesting())
}
}
struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
let content: () -> T
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: content())
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}
Tried using Hieu's solution in a navigation bar item but it was breaking. Modified it by directly using SwiftUIWrapper and allowsHitTesting on the component I want to display and it works like a charm.
Also works on List and Form
struct StealthDatePicker: View {
#State private var date = Date()
var body: some View {
ZStack {
DatePicker("", selection: $date, in: ...Date(), displayedComponents: .date)
.datePickerStyle(.compact)
.labelsHidden()
SwiftUIWrapper {
Image(systemName: "calendar")
.resizable()
.frame(width: 32, height: 32, alignment: .topLeading)
}.allowsHitTesting(false)
}
}
}
struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
let content: () -> T
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: content())
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}
My answer to this was much simpler... just create a button with a popover that calls this struct I created...
struct DatePopover: View {
#Binding var dateIn: Date
#Binding var isShowing: Bool
var body: some View {
VStack {
DatePicker("", selection: $dateIn, displayedComponents: [.date])
.datePickerStyle(.graphical)
.onChange(of: dateIn, perform: { value in
isShowing.toggle()
})
.padding(.all, 20)
}.frame(width: 400, height: 400, alignment: .center)
}
}
Not sure why, but it didn't format my code like I wanted...
( Original asnwer had button, onChange is better solution)
Sample of my Button that calls it... it has my vars in it and may not make complete sense to you, but it should give you the idea and use in the popover...
Button(item.dueDate == nil ? "" : dateValue(item.dueDate!)) {
if item.dueDate != nil { isUpdatingDate = true }
}
.onAppear { tmpDueDate = item.dueDate ?? .now }
.onChange(of: isUpdatingDate, perform: { value in
if !value {
item.dueDate = tmpDueDate
try? moc.save()
}
})
.popover(isPresented: $isUpdatingDate) {
DatePopover(dateIn: $tmpDueDate, isShowing: $isUpdatingDate)
}
FYI, dateValue() is a local func I created - it simply creates a string representation of the Date in my format
struct ZCalendar: View {
#State var date = Date()
#State var isPickerVisible = false
var body: some View {
ZStack {
Button(action: {
isPickerVisible = true
}, label: {
Image(systemName: "calendar")
}).zIndex(1)
if isPickerVisible{
VStack{
Button("Done", action: {
isPickerVisible = false
}).padding()
DatePicker("", selection: $date).datePickerStyle(GraphicalDatePickerStyle())
}.background(Color(UIColor.secondarySystemBackground))
.zIndex(2)
}
}//Another way
//.sheet(isPresented: $isPickerVisible, content: {DatePicker("", selection: $date).datePickerStyle(GraphicalDatePickerStyle())})
}
}
For those still looking for a simple solution, I was looking for something similar and found a great example of how to do this in one of Kavasoft's tutorials on YouTube at 20:32 into the video.
This is what he used:
import SwiftUI
struct DatePickerView: View {
#State private var birthday = Date()
#State private var isChild = false
#State private var ageFilter = ""
var body: some View {
Image(systemName: "calendar")
.font(.title3)
.overlay{ //MARK: Place the DatePicker in the overlay extension
DatePicker(
"",
selection: $birthday,
displayedComponents: [.date]
)
.blendMode(.destinationOver) //MARK: use this extension to keep the clickable functionality
.onChange(of: birthday, perform: { value in
isChild = checkAge(date:birthday)
})
}
}
//MARK: I added this function to show onChange functionality remains the same
func checkAge(date: Date) -> Bool {
let today = Date()
let diffs = Calendar.current.dateComponents([.year], from: date, to: today)
let formatter = DateComponentsFormatter()
let outputString = formatter.string(from: diffs)
self.ageFilter = outputString!.filter("0123456789.".contains)
let ageTest = Int(self.ageFilter) ?? 0
if ageTest > 18 {
return false
}else{
return true
}
}
}
The key is put the DatePicker in an overlay under the Image. Once done, the .blendmode extension needs to be set to .desintationOver for it to be clickable. I added a simple check age function to show onChange functionality remains the same when using it in this way.
I tested this code in Xcode 14 (SwiftUI 4.0 and IOS 16).
I hope this helps others!
Demo
Please understand that my sentence is weird because I am not good at English.
In the code above, if you use .frame() & .clipped().
Clicks can be controlled exactly by the icon size.
In the code above, I modified it really a little bit.
I found the answer.
Thank you.
import SwiftUI
struct DatePickerView: View {
#State private var date = Date()
var body: some View {
ZStack{
DatePicker("", selection: $date, displayedComponents: .date)
.labelsHidden()
.datePickerStyle(.compact)
.frame(width: 20, height: 20)
.clipped()
SwiftUIWrapper {
Image(systemName: "calendar")
.resizable()
.frame(width: 20, height: 20, alignment: .topLeading)
}.allowsHitTesting(false)
}//ZStack
}
}
struct DatePickerView_Previews: PreviewProvider {
static var previews: some View {
DatePickerView()
}
}
struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
let content: () -> T
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: content())
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}

Prevent SwiftUI animation from overriding nested withAnimation block

I have a screen with a draggable component (the "Springy" text) that springs back when released with a very prominent spring animation.
There is some text that comes in asynchronously (in this example, using a Timer), and I want that to fade in nicely while the Springy label moves up smoothly to make room for it.
I added an animation to the VStack, and it transitions as I want it to when the asynchronously-loaded text fades in. However, that breaks the spring animation on the "Springy" text.
There is an animation that's commented out here, and if switch the 1 second animation to that spot the asynchronously-loaded text fades in, but right at the beginning of the animation the "Springy" text jumps up instead of sliding up.
How can I get both animations to work as I want them to?
import SwiftUI
import Combine
class Store: ObservableObject {
#Published var showSection: Bool = false
private var cancellables: [AnyCancellable] = []
init() {
Timer
.publish(every: 2, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in true}
.assign(to: \.showSection,
on: self)
.store(in: &cancellables)
}
}
struct ContentView: View {
#ObservedObject var store = Store()
#State var draggedState = CGSize.zero
var body: some View {
VStack {
Spacer()
VStack(alignment: .leading) {
VStack(spacing: 4) {
HStack {
Text("Springy")
.font(.title)
Image(systemName: "hand.point.up.left.fill")
.imageScale(.large)
}
.offset(x: draggedState.width, y: draggedState.height)
.foregroundColor(.secondary)
.gesture(
DragGesture().onChanged { value in
draggedState = value.translation
}
.onEnded { value in
// How can this animation be fast without the other animation slowing it down?
withAnimation(Animation
.interactiveSpring(response: 0.3,
dampingFraction: 0.1,
blendDuration: 0)) {
draggedState = .zero
}
}
)
VStack {
if store.showSection {
Text("Something that animates in after loading asynchrously.")
.font(.body)
.padding()
.transition(.opacity)
}
}
// When the animation is here, it doesn't cancel out the spring animation, but the Springy text jumps up at the beginning of the animation instead of animating.
// .animation(.easeInOut(duration: 1))
}
// Animation here has desired effect for loading, but overrides the Springy release animation.
.animation(.easeInOut(duration: 1))
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}
Expanded question, based on answer from Asperi.
Now, supposing I have multiple elements in this main VStack that I want to animate when they come in.
Is it a good solution to add a separate .animation modifier with a value for each section I want to animate?
In the expanded example below, that's the part with:
.animation(.easeInOut(duration: 1),
value: store.showSection)
.animation(.easeInOut(duration: 1),
value: store.somePossibleText)
.animation(.easeInOut(duration: 1),
value: store.moreInformation)
I am using 3 approaches here to determine whether to show a section or not. I used Bool in the initial example for simplicity, but in my real scenario I think it will make the most sense to use the String? approach instead of setting a separate Bool or checking for an empty string. Are there drawbacks to using that when it comes to animations, or is that fine? It seems to work well in this example given Asperi's solution.
Here's the full example again with the new modifications:
import SwiftUI
import Combine
class Store: ObservableObject {
#Published var showSection: Bool = false
#Published var somePossibleText: String = ""
#Published var moreInformation: String?
private var cancellables: [AnyCancellable] = []
init() {
Timer
.publish(every: 2, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in true }
.assign(to: \.showSection,
on: self)
.store(in: &cancellables)
Timer
.publish(every: 3, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in "Something else loaded later" }
.assign(to: \.somePossibleText,
on: self)
.store(in: &cancellables)
Timer
.publish(every: 4, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in "More stuff loaded later" }
.assign(to: \.moreInformation,
on: self)
.store(in: &cancellables)
}
}
struct ContentView: View {
#ObservedObject var store = Store()
#State var draggedState = CGSize.zero
var body: some View {
VStack {
Spacer()
VStack(alignment: .leading) {
VStack(spacing: 4) {
HStack {
Text("Springy")
.font(.title)
Image(systemName: "hand.point.up.left.fill")
.imageScale(.large)
}
.offset(x: draggedState.width,
y: draggedState.height)
.foregroundColor(.secondary)
.gesture(
DragGesture().onChanged { value in
draggedState = value.translation
}
.onEnded { value in
// TODO: how can this animation be fast without the other one slowing it down?
withAnimation(Animation
.interactiveSpring(response: 0.3,
dampingFraction: 0.1,
blendDuration: 0)) {
draggedState = .zero
}
}
)
if store.showSection {
Text("Something that animates in after loading asynchrously.")
.font(.body)
.padding()
.transition(.opacity)
}
if store.somePossibleText != "" {
Text(store.somePossibleText)
.font(.footnote)
.padding()
.transition(.opacity)
}
if let moreInformation = store.moreInformation {
Text(moreInformation)
.font(.footnote)
.padding()
.transition(.opacity)
}
}
.animation(.easeInOut(duration: 1),
value: store.showSection)
.animation(.easeInOut(duration: 1),
value: store.somePossibleText)
.animation(.easeInOut(duration: 1),
value: store.moreInformation)
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}
If I correctly understood your expectation, it is enough to bind last animation to showSection value, like below
}
// Animation here has desired effect for loading, but overrides the Springy release animation.
.animation(.easeInOut(duration: 1), value: store.showSection)
Tested with Xcode 12 / iOS 14.

Why does the #Published value not get triggered in SwiftUI ObservableObject?

I have create the below contrived example to demonstrate my problem. In this code I am expecting the TextField to be initialised with "Test Company X" where X is the view depth based on the ObservableObject ViewModel. This is true and works fine for the first view only. Subsequent views do not get the published value on initial appearance. However as you back through the views and the onAppear triggers it does get initialised. Why is the TextField not correctly initialised on the second and subsequent views?
class TestViewModel: ObservableObject {
#Published var companyName: String = ""
func load(_ viewDepth: Int) {
debugPrint("load: \(viewDepth)")
// Mimic a network request to get data
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
debugPrint("publish: \(viewDepth)")
self.companyName = "Test Company \(viewDepth)"
}
}
}
struct TestFormView: View {
var viewDepth: Int
#Binding var companyName: String
#State private var navigateToNextView: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 3) {
TextField("Company name", text: $companyName)
.padding()
.background(
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color.primary.opacity(0.5), lineWidth: 3)
)
Button.init("NEXT") {
self.navigateToNextView = true
}
.frame(maxWidth: .infinity)
.foregroundColor(Color.primary)
.padding(10)
.font(Font.system(size: 18,
weight: .semibold,
design: .default))
.background(Color.secondary)
.cornerRadius(Sizes.cornerRadius)
NavigationLink(destination: TestView(viewDepth: viewDepth + 1), isActive: $navigateToNextView) {
EmptyView()
}
.isDetailLink(false)
Spacer()
}
}
}
struct TestView: View {
#ObservedObject var viewModel = TestViewModel()
var viewDepth: Int
var body: some View {
VStack {
TestFormView(viewDepth: viewDepth, companyName: $viewModel.companyName)
}
.onAppear(perform: {
self.viewModel.load(self.viewDepth)
})
.navigationBarTitle("View Depth \(viewDepth)")
.padding()
}
}
struct TestNavigationView: View {
#State private var begin: Bool = false
var body: some View {
NavigationView {
VStack {
if begin {
TestView(viewDepth: 1)
} else {
Button.init("BEGIN") {
self.begin = true
}
.frame(maxWidth: .infinity)
.foregroundColor(Color.primary)
.padding(10)
.font(Font.system(size: 18,
weight: .semibold,
design: .default))
.background(Color.secondary)
.cornerRadius(Sizes.cornerRadius)
}
}
}
}
}
Your model is recreated (due to current nature of NavigationLink)
SwiftUI 2.0
Fix is simple - use specially intended for such purpose StateObject
struct TestView: View {
#StateObject var viewModel = TestViewModel()
// .. other code

Resources