Swift UI DatePicker won't update #ObservedObject value - swift

I am using a class to store a #Published variable but when I try to pass the value chosen on my custom DatePicker to the #ObservedObject in the class I receive the following error:
TimePicker(time: self.$time.**timeSelected**)
Cannot convert value of type 'Binding' (aka
'Binding') to expected argument type 'TimeModel'
How do I update the #ObservedObject with the picker value?
Full code:
struct ContentView: View {
#ObservedObject var time = TimeModel()
var body: some View {
ZStack{
VStack{
TimePicker(time: self.$time.timeSelected)
Text("You chose \(time.timeSelected/60) minutes")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class TimeModel: ObservableObject{
#Published var timeSelected: TimeInterval = 1.0
}
struct TimePicker: UIViewRepresentable {
#ObservedObject var time = TimeModel()
func makeUIView(context: Context) -> UIDatePicker {
let datePicker = UIDatePicker()
datePicker.datePickerMode = .countDownTimer
datePicker.addTarget(context.coordinator,
action: #selector(Coordinator.updateTime),
for: .valueChanged)
return datePicker
}
func updateUIView(_ datePicker: UIDatePicker, context: Context) {
datePicker.minuteInterval = 5
datePicker.countDownDuration = time.timeSelected
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
let parent: TimePicker
init(_ parent: TimePicker) {
self.parent = parent
}
#objc func updateTime(datePicker: UIDatePicker) {
parent.time.timeSelected = datePicker.countDownDuration
}
}
}

You need to refer to the same TimeModel instance.
A solution is to pass the TimeModel to the TimePicker:
struct TimePicker: UIViewRepresentable {
#ObservedObject var time: TimeModel // <- declare only
...
}
struct ContentView: View {
#ObservedObject var time = TimeModel()
var body: some View {
ZStack {
VStack {
TimePicker(time: time) // <- pass `TimeModel` here
Text("You chose \(time.timeSelected / 60) minutes")
}
}
}
}
Note that TimeInterval specifies time is in seconds, not minutes.
This line:
#Published var timeSelected: TimeInterval = 1.0
actually sets the default time to 1 second.

Related

PreviewProvider and ObservedObject properties

how can I set a property to my CoreData Object which has the type CDObject, it has a property called name: String
My issue is now that I do not know how to set the name property in the PreviewProvider
Here is the code:
struct MainView: View {
#ObservedObject var obj: CDObject
var body: some View {
Text("Hello, World!")
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView(obj: CDObject())
}
}
I would like to do something like, before passing it to the View:
let itm = CDObject()
itm.name = "Hello"
If you are using the standard PersistenceController that comes with Xcode when you start a new project with CoreData just add the below method so Xcode returns the .preview container when you are running in preview.
public static func previewAware() -> PersistenceController{
//Identifies if XCode is running for previews
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"{
return PersistenceController.preview
}else{
return PersistenceController.shared
}
}
As for the rest you can use something like this.
import SwiftUI
import CoreData
struct SamplePreviewView: View {
#ObservedObject var item: Item
var body: some View {
Text(item.timestamp?.description ?? "nil")
}
}
struct SamplePreviewView_Previews: PreviewProvider {
static let svc = CoreDataPersistenceService()
static var previews: some View {
SamplePreviewView(item: svc.addSample())
}
}
class CoreDataPersistenceService: NSObject {
var persistenceController: PersistenceController
init(isTest: Bool = false) {
if isTest{
self.persistenceController = PersistenceController.preview
}else{
self.persistenceController = PersistenceController.previewAware()
}
super.init()
}
func addSample() -> Item {
let object = createObject()
object.timestamp = Date()
return object
}
//MARK: CRUD methods
func createObject() -> Item {
let result = Item.init(context: persistenceController.container.viewContext)
return result
}
}

UIViewRepresentable UITextField skips first character when connected to ObservableObject's #Published property

I have a CustomTextField conforming to UIViewRepresentable. This field is connected to the ViewModel's #Published email property. The ViewModel itself conforms to ObservableObject. When I launch the app on iPhone 12 Pro Max (14.3) simulator, whenever I try to type the first character, it doesn't appear in the field nor it gets added the email's value. The consecutive characters are typed just fine.
Here is the code:
import SwiftUI
class ViewModel: ObservableObject {
#Published var email = ""
}
struct ContentView: View {
#ObservedObject var vm = ViewModel()
var body: some View {
ZStack {
Color.blue
CustomTextField(text: $vm.email, placeholder: "Email")
.frame(height: 40)
.padding()
}
.ignoresSafeArea()
}
}
struct CustomTextField: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.borderStyle = .roundedRect
textField.placeholder = placeholder
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.spellCheckingType = .no
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ view: UITextField, context: Context) {
view.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
self.text = textField.text ?? ""
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Does anyone know what's wrong here?
If you initialize your model in View, you should use #StateObject instead of #ObservedObject.
Also, you may try assigning textField.text = text in the makeUIView method.
And I feel like this approach for Representable objects is more elegant, take a look ^^
https://github.com/twostraws/VisualEffects
The problem was in Xcode. Once I upgraded to 12.4, the issue went away.

#State updates view but #ObservedObject does not

I have a view:
struct Form: View {
#ObservedObject var model = FormInput()
var body: some View {
Form {
TextArea("Details", text: $model.details.value)
.validation(message: model.details.message)
}
}
}
Where TextArea is a custom view and .validation(message: model.details.message) is a custom view modifier. My FormInput looks as follows:
final class FormInput: ObservableObject {
#Published var details: TextInput
// ... other code
}
TextInput is a custom struct.
Now when I run the above code the validation never triggers because the Form never re-renders, but if I change the Form to:
struct MyForm: View {
#State var details = TextInput()
var body: some View {
Form {
TextArea("Details", text: $details.value)
.validation(message: details.message)
}
}
}
Then everything works as expected. Why would the view render for the second version of Form but not for the first? Shouldn't the Form in the first case update when details changes since details is #Published and the Form is observing the changes?
ADDITIONAL CONTEXT
Below is additional code for the above components
final class FormInput: ObservableObject {
#Published var details: TextInput
init(details: String = "") {
self.details = TextInput(value: details, isValid: false, validations: [.length(12)])
}
// other code
}
struct TextInput {
var value: String {
didSet {
self.validate()
}
}
var validations: [TextValidation] // this is just an enum of different types of validations
var isValid: Bool
var message: String
mutating func validate() {
for validation in validations {
// run validation
}
}
}
struct TextArea: View {
#Binding var text: String
#State var height: CGFloat = 12
init(text: Binding<String>) {
self._text = text
}
var body: some View {
TextAreaField(text: $text)
}
}
struct TextAreaField: UIViewRepresentable {
#Binding var text: String
#Binding var height: CGFloat
func makeUIView(context: Context) -> UITextView {
let textField = UITextView()
textField.isEditable = true
textField.isSelectable = true
textField.isUserInteractionEnabled = true
textField.isScrollEnabled = false
// ..other initializers removed for brevity
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextView, context: Context) {
calculateHeight(uiView)
}
func calculateHeight(_ uiView: UIView) {
let newSize = uiView.sizeThatFits(CGSize(width: uiView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if self.height != newSize.height {
DispatchQueue.main.async {
self.height = newSize.height
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
final class Coordinator: NSObject, UITextViewDelegate {
var parent: TextAreaField
init(_ parent: TextAreaField) {
self.parent = parent
}
func textViewDidChange(_ uiView: UITextView) {
self.parent.text = uiView.text
self.parent.calculateHeight(uiView)
}
}
}
struct Validation: ViewModifier {
let message: String
func body(content: Content) -> some View {
let isValid = message == ""
print(message)
return VStack(alignment: .leading) {
content
if isValid == false {
Text(message)
.font(.footnote)
.italic()
.foregroundColor(Color.red)
.frame(height: 24)
}
}
.padding(.bottom, isValid ? 24 : 0)
}
}
extension View {
func validation(message: String) -> some View {
self.modifier(Validation(message: message))
}
}
I suppose the issue is in absent custom components. Because below simple demo replication of provided infrastructure works well with ObservableObject, actually as expected.
Tested with Xcode 11.4 / iOS 13.4. Comparing with below demo might be helpful to find what is missed in your code.
struct TextInput {
var value: String = "" {
didSet {
message = value // just duplication for demo
}
}
var message: String = ""
}
// simple validator highlighting text when too long
struct ValidationModifier: ViewModifier {
var text: String
func body(content: Content) -> some View {
content.foregroundColor(text.count > 5 ? Color.red : Color.primary)
}
}
extension View { // replicated
func validation(message: String) -> some View {
self.modifier(ValidationModifier(text: message))
}
}
struct MyForm: View { // avoid same names with standard views
#ObservedObject var model = FormInput()
var body: some View {
Form {
TextField("Details", text: $model.details.value) // used standard
.validation(message: model.details.message)
}
}
}
final class FormInput: ObservableObject {
#Published var details: TextInput = TextInput()
}
struct TestObservedInModifier: View {
var body: some View {
MyForm()
}
}

How can I update view state in response to external changes?

Imagine I have a view with some mutable state, but that the state might need to be updated to reflect changes in another object (e.g. a ViewModel).
How can I implement that in SwiftUI?
I've tried the following, but can't get the view to reflect updates coming from the ViewModel:
class ViewModel: ObservableObject {
#Published var text: String = "loading"
private var task: AnyCancellable?
func fetch() {
task = Just("done")
.delay(for: 1, scheduler: RunLoop.main)
.assign(to: \.text, on: self)
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#State var viewText = "idle"
private var bind: AnyCancellable?
init() {
viewText = viewModel.text
bind = viewModel
.$text
.print()
.assign(to: \.viewText, on: self)
}
var body: some View {
VStack {
TextField(titleKey: "editable text", text: $viewText)
Text(viewText)
Text(viewModel.text)
}
.onAppear {
self.viewModel.fetch()
}
}
}
The TextField and the first Text element get their content from ContentView.viewText, the second Text goes directly to the source: ViewModel.text.
As expected, the second Text shows "loading" and then "done". The first Text never changes from "idle".
If next screen recording looks like answering your question
it was recorded using next code
import SwiftUI
import Combine
class ViewModel: ObservableObject {
#Published var text: String = "loading"
private var task: AnyCancellable?
func fetch() {
task = Just("done")
.delay(for: 3, scheduler: RunLoop.main)
.assign(to: \.text, on: self)
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#State var viewText = "idle"
var body: some View {
VStack {
Text(viewText)
Text(viewModel.text)
}.onReceive(viewModel.$text.filter({ (s) -> Bool in
s == "done"
})) { (txt) in
self.viewText = txt
}.onAppear {
self.viewModel.fetch()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is possible approach (tested & works with Xcode 11.2 / iOS 13.2) - modified only ContentView:
struct ContentView: View {
#ObservedObject var viewModel = ViewModelX()
#State private var viewText = "idle"
init() {
_viewText = State<String>(initialValue: viewModel.text)
}
var body: some View {
VStack {
Text(viewText)
Text(viewModel.text)
}
.onReceive(viewModel.$text) { value in
self.viewText = value
}
.onAppear {
self.viewModel.fetch()
}
}
}

DatePicker using Time-Interval in SwiftUI

I want to use a DatePicker in SwiftUI, it is working fine and as expected. I want to add an Time-interval, like explained: UIDatePicker 15 Minute Increments Swift
DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
Is there a Modifier for that in SwifUI?
I don't believe there is a modifier for this. However it's possible to "do it yourself" by using UIViewRepresentable to wrap a UIDatePicker:
The basic structure for this code is based on the Interfacing with UIKit tutorial.
struct MyDatePicker: UIViewRepresentable {
#Binding var selection: Date
let minuteInterval: Int
let displayedComponents: DatePickerComponents
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<MyDatePicker>) -> UIDatePicker {
let picker = UIDatePicker()
// listen to changes coming from the date picker, and use them to update the state variable
picker.addTarget(context.coordinator, action: #selector(Coordinator.dateChanged), for: .valueChanged)
return picker
}
func updateUIView(_ picker: UIDatePicker, context: UIViewRepresentableContext<MyDatePicker>) {
picker.minuteInterval = minuteInterval
picker.date = selection
switch displayedComponents {
case .hourAndMinute:
picker.datePickerMode = .time
case .date:
picker.datePickerMode = .date
case [.hourAndMinute, .date]:
picker.datePickerMode = .dateAndTime
default:
break
}
}
class Coordinator {
let datePicker: MyDatePicker
init(_ datePicker: MyDatePicker) {
self.datePicker = datePicker
}
#objc func dateChanged(_ sender: UIDatePicker) {
datePicker.selection = sender.date
}
}
}
struct DatePickerDemo: View {
#State var wakeUp: Date = Date()
#State var minterval: Int = 1
var body: some View {
VStack {
Stepper(value: $minterval) {
Text("Minute interval: \(minterval)")
}
MyDatePicker(selection: $wakeUp, minuteInterval: minterval, displayedComponents: .hourAndMinute)
Text("\(wakeUp)")
}
}
}
struct DatePickerDemo_Previews: PreviewProvider {
static var previews: some View {
DatePickerDemo()
}
}
Not fully a SwiftUI solution, but much simpler than the existing answer. Attach this to your main body View that contains the picker
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
}
The one downside is this will apply to all pickers in the view and can affect pickers in other views in the app, but you can always do the same thing in those views (set minute interval back to 1).
I think, the below will help..
struct DatePickerView: View {
#Binding var dateSelected: Date
var type: DatePickerComponents? = .date
var body: some View {
DatePicker("", selection: $dateSelected, displayedComponents: .hourAndMinute)
.datePickerStyle(.wheel)
.onAppear {
if type == .hourAndMinute {
UIDatePicker.appearance().minuteInterval = 15
} else {
UIDatePicker.appearance().minimumDate = dateSelected
}
}
}
}
It is also possible with Introspect lib
DatePicker(...)
.introspectDatePicker {
$0.minuteInterval = 30
$0.roundsToMinuteInterval = true
}