swiftui Properties binding to each other and swiftui - swift

in my Swiftui project I have a picker and a graph image with a dropped pin. When I update the picker the pin changes location after transforming for coordinates. I would also like to let the user move the pin and have it appropriately update the picker selection. How do I bind the picker selection and pin location without causing an infinite loop(user moves pin which updates picker which then tries to update pin and so on)
Currently I have two observable classes - AppState and GraphState - Heres a sample of what I'm doing. Tried to simplify it, so I may have missed some details
class AppState: ObservableObject {
#Published var pickerSelection:String
var graphState = GraphState()
init() {
GraphStateSubscribe()
}
func GraphStateSubscribe(){
graphState.state = self
$pickerSelection.subscribe(self.graphState)
}
}
class GraphState:ObsevableObject{
#Published var pinLocation: {
willSet {
state!.pickerSelection = newValue
}
var state:AppState?
/// func placePinCode here
}
extension GraphState:Subscriber{
typealias NewSelectionValue = String
typealias Input = NewSelectionValue
typealias Failure = Never
func receive(subscription: Subscription) {
print("Received graphState subscription")
subscription.request(.unlimited)
}
func receive(_ input: (NewSelectionValue)) -> Subscribers.Demand {
placePin(input)
return .none
}
func receive(completion: Subscribers.Completion<Never>) {
print("completed graphState subscription")
}
}
these are each bound to swiftuiviews which then draw from and update the individual state classes on user interaction. The reason I separate them is because each state class also has other functions its doing.
I found a somewhat related solution in RxSwift - https://dev.to/vaderdan/rxswift-reverse-observable-aka-two-way-binding-5e5n
Whats the best way to keep these two properties in sync?
thanks for your help

Related

Strange behavior of #Binding variable in SwiftUI integrating UIKit (iOS app)

I am developing an App for my school's courses, displaying the descriptions of the courses as well as a calendar showing the appointments. I decided to use KVKCalendar as a library for the Calendar. Developing with SwiftUI and being the KVKCalendar library a UIKit environment, I had to interface the two systems (with the struct CalendarRepresentable, see code).
The app is constructed as follow: I load from the memory an #ObservedObject with the saved objects, pass it to some sub-views as #Binding in order to save/delete new courses from the course-catalogue.
All changes made in the views are updated accordingly in all the other views, but unfortunately not in the TimetableView, connected with the struct CalendarRepresentable.
The problem: I want to update the Calendar whenever new courses are saved (e.g. added to the array courses of the type [Courses]). At the moment the struct CalendarRepresentable is calling the makeUIView as expected but the calendar is not updated anymore. In particular the saved courses in courses are not constantly updated but in a (apparently) inconsitent way: in some functions of the struct CalendarRepresentable are indeed up to date, in some other functions (and subclasses) not. See code where I show where and in what case these inconsistencies occure.
Code of the View calling the UIKit-SwiftUI interface:
import SwiftUI
import KVKCalendar
struct TimetableView: View {
#Binding var courses: [Courses]
let saveAction: () -> Void
var body: some View {
CalendarRepresentable(courses: $courses)
}
}
struct TimetableView_Previews: PreviewProvider {
static var previews: some View {
TimetableView(courses: .constant(Courses.data), saveAction: {})
}
}
Code of the interface:
import SwiftUI
import KVKCalendar
import EventKit
struct CalendarRepresentable: UIViewRepresentable{
#Binding var courses: [Courses]
var events = [Event]()
var calendar: CalendarView = {
print("Representable has been launched")
var style = Style()
//style of the calendar
//...
return CalendarView(frame: UIScreen.main.bounds, style: style)
}()
func updateUIView(_ uiView: CalendarView, context: Context ){
print("updateUIView has been called. Courses has \(courses.count) elements") // **Here courses has always the correct amount of elements**
calendar.dataSource = context.coordinator
calendar.delegate = context.coordinator
calendar.reloadData()
calendar.reloadInputViews()
print("updateUIView is finished")
}
public func passCourses() -> [Courses]{
print("Called passCourses with courses having \(courses.count) elements") //**Here courses has NOT the corrent amount of elements, i.e. after saving/deleting a course courses.count is different from what is printed in the previous function updateUIView**
return courses
}
func makeUIView(context: Context) -> CalendarView {
print("makeUIView has been called")
calendar.dataSource = context.coordinator
calendar.delegate = context.coordinator
calendar.reloadData()
return calendar
}
func makeCoordinator() -> Coordinator {
print("makeCoordinator")
return Coordinator(self)
}
class Coordinator: NSObject, CalendarDelegate, CalendarDataSource {
var events = [Event]()
var parent: CalendarRepresentable
func eventsForCalendar(systemEvents: [EKEvent]) -> [Event] {
print("eventsForCalendar called. In Coordinator \(events.count) events and \(parent.courses.count) courses") //**here courses are again not uptodate to the CalendarRepresentable' courses**
loadEvents { (events) in
self.events = events
}
return self.events
}
init(_ parent: CalendarRepresentable){
print("Initialize Coordinator")
self.parent = parent
super.init()
loadEvents { (events) in
self.events = events
self.parent.calendar.reloadData()
}
}
func eventsForCalendar() -> [Event] {
print("eventsForCalendar without parameter")
return events
}
func loadEvents(completion: ([Event]) -> Void) {
var events = [Event]()
print("loadEvents with courses having \(self.parent.courses.count) elements")//**here courses are again not uptodate to the CalendarRepresentable' courses**
var i: Int=0
for course in self.parent.passCourses() {
let isoDateStart = course.startEvent
let dateStartFormatter = ISO8601DateFormatter()
let isoDateEnd = course.endEvent
let dateEndFormatter = ISO8601DateFormatter()
var event = Event(ID: "\(i)")
event.start = dateStartFormatter.date(from: isoDateStart)!
event.end = dateEndFormatter.date(from: isoDateEnd)!
event.text = course.name
events.append(event)
i=i+1
}
completion(events)
}
}
}
Example of console output after saving a course (done in another view) with (apparent) inconsistency:
Representable has been launched
updateUIView has been called. Courses has 4 elements
eventsForCalendar called. In Coordinator 3 events and 3 courses
loadEvents with courses having 3 elements
Called passCourses with courses having 3 elements
updateUIView is finished
Does anyone know what is happening to courses? Suggestions on how should I solve the issue?
Thanks a lot for your time and help!
After extensive testing I found that the problem was that somehow the coordinator had been called with an old parent although the outerstruct had been changed. I am not sure about the internal cause of that behavior, but anyway it should now be solved: the courses data is now successfully passed to the coordinator. However, the view still doesn't update itself despite a change in courses correctly triggering updateUIView. Any advice is appreciated.

How can I Observe a var in a Class?

Here is my code for a simple class, My goal is that observeValueOfModel() function automatically put changes of valueOfModel under control and print the correct message out!
I can manually use this func for getting the Answer, but the goal is this class be able understand and react to value change of valueOfModel. Thanks for help
class Model: ObservableObject {
var valueOfModel: Bool = Bool()
private func observeValueOfModel() {
if valueOfModel {
print("valueOfModel is True!")
}
else {
print("valueOfModel is False!")
}
}
}
The didSet fits in this case
class Model: ObservableObject {
var valueOfModel: Bool = Bool() {
didSet {
observeValueOfModel()
}
}
// ... other code
Combine will help you. Define your var as #Published to be able to subscribe to it
#Published var valueOfModel: Bool = true
You can subscribe to changes in the init or viewDidLoad for example. We store the subscription in the cancelable. Put it in the VC or as class property to keep the subscription alive.
let cancelable: AnyCancellable?
cancelable = valueOfModel.sink { [weak self] value
// this will get called as soon as valueOfModel gets updated
// do smth with value here
}

How to stop variable from going back to default value when read in different file

I have a DiscoveredSerialNumbers class that I want to access from various swift files:
class DiscoveredSerialNumbers {
var snConnect: String = ""
}
In my ViewController I change the value of snConnect based on the selection from a Picker View.
class ViewController: UIViewController, UIPickerViewDataSource,UIPickerViewDelegate {
#IBOutlet weak var SerialNumbers: UIPickerView!
var serialNums: [String] = [String]()
...
override func viewDidLoad() {
...
SerialNumbers.dataSource = self
SerialNumbers.delegate = self
}
...
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let global = DiscoveredSerialNumbers()
global.snConnect = serialNums[row]
print(serialNums[row])
print(global.snConnect)
}
}
When I print out the new value of snConnect set in the following line:
global.snConnect = serialNums[row]
Immediately afterward I get the new updated value of snConnect.
However, when I try to access the updated value of snConnect in a different swift file that controls a different ViewController in the following code:
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
let global = DiscoveredSerialNumbers()
var sn = global.snConnect
...
}
The value of snConnect reverts back to the original value which is "".
How do I stop the value from reverting back to the initial value? I think it has something to do with me initializing the class DiscoveredSerialNumbers but I do not know how to access the value of snConnect in a different swift file otherwise.
Edit: Thanks to Don's comments, I am trying to have the snConnect value persist between instances of the application launching. I want to set the value of snConnect in the main app and access it when I launch an extension to the main app, in this case a custom keyboard extension.
Update: Question was a bit misleading you actually need to save the variable. I'm not sure if UserDefaults for app and keyboard extension are the same, you can try this.
class DiscoveredSerialNumbers {
static var main = DiscoveredSerialNumbers()
var snConnect: String {
get {
// Read from UserDefaults
return UserDefaults.standard.string(forKey: "snConnect") ?? ""
}
set {
// Save to UserDefaults
UserDefaults.standard.set(newValue, forKey: "snConnect")
}
}
init() {
print("New instance of DiscoveredSerialNumbers initialized.")
}
}
You can do this with a number of different ways,
however easiest one is creating a singleton of DiscoveredSerialNumbers() object, so your object and values can be used globally through it.
(although this method should be used with caution, it can cause a number of problems)
class DiscoveredSerialNumbers {
static var main = DiscoveredSerialNumbers()
var snConnect: String = ""
init() {
print("New instance of DiscoveredSerialNumbers initialized.")
}
}
now whenever you call DiscoveredSerialNumbers.main.snConnect old value will be kept and can be used/changed from anywhere.
Edit: Here's a sample Playground code for you to test out how singletons work
class Singleton
{
var someVariable = ""
static var main = Singleton()
}
class ClassA
{
init() {
Singleton.main.someVariable = "Hey I was changed in Class A"
}
}
class ClassB
{
init() {
print(Singleton.main.someVariable)
Singleton.main.someVariable = "And now I'm changed in class B"
}
}
let _ = ClassA()
let _ = ClassB()
print(Singleton.main.someVariable)
For an app extension to access data stored through it's container app, both the application and extension need to be part of the same app group. App groups are set in Signing & Capabilities section of Xcode for your project.
Once your app and extension are part of the same app group, you can use the following code to set the value of a global variable:
let defaults = UserDefaults(suiteName:"group.dataShare")
defaults?.set(serialNums[row], forKey: "snConnect")
Where group.dataShare is the name of your App group.
To retrieve the value, you can use the following code in your extension:
let defaults = UserDefaults(suiteName:"group.dataShareImada")
var sn = defaults?.string(forKey: "snConnect")

How do you call a method on a UIView from outside the UIViewRepresentable in SwiftUI?

I want to be able to pass a reference to a method on the UIViewRespresentable (or perhaps it’s Coordinator) to a parent View. The only way I can think to do this is by creating a field on the parent View struct with a class that I then pass to the child, which acts as a delegate for this behaviour. But it seems pretty verbose.
The use case here is to be a able to call a method from a standard SwiftUI Button that will zoom the the current location in a MKMapView that’s buried in a UIViewRepresentable elsewhere in the tree. I don’t want the current location to be a Binding as I want this action to be a one off and not reflected constantly in the UI.
TL;DR is there a standard way of having a parent get a reference to a child in SwiftUI, at least for UIViewRepresentables? (I understand this is probably not desirable in most cases and largely runs against the SwiftUI pattern).
I struggled with that myself, here's what worked using Combine and PassthroughSubject:
struct OuterView: View {
private var didChange = PassthroughSubject<String, Never>()
var body: some View {
VStack {
// send the PassthroughSubject over
Wrapper(didChange: didChange)
Button(action: {
self.didChange.send("customString")
})
}
}
}
// This is representable struct that acts as the bridge between UIKit <> SwiftUI
struct Wrapper: UIViewRepresentable {
var didChange: PassthroughSubject<String, Never>
#State var cancellable: AnyCancellable? = nil
func makeUIView(context: Context) → SomeView {
let someView = SomeView()
// ... perform some initializations here
// doing it in `main` thread is required to avoid the state being modified during
// a view update
DispatchQueue.main.async {
// very important to capture it as a variable, otherwise it'll be short lived.
self.cancellable = didChange.sink { (value) in
print("Received: \(value)")
// here you can do a switch case to know which method to call
// on your UIKit class, example:
if (value == "customString") {
// call your function!
someView.customFunction()
}
}
}
return someView
}
}
// This is your usual UIKit View
class SomeView: UIView {
func customFunction() {
// ...
}
}
I'm sure there are better ways, including using Combine and a PassthroughSubject. (But I never got that to work.) That said, if you're willing to "run against the SwiftUI pattern", why not just send a Notification? (That's what I do.)
In my model:
extension Notification.Name {
static let executeUIKitFunction = Notification.Name("ExecuteUIKitFunction")
}
final class Model : ObservableObject {
#Published var executeFuntionInUIKit = false {
willSet {
NotificationCenter.default.post(name: .executeUIKitFunction, object: nil, userInfo: nil)
}
}
}
And in my UIKit representable:
NotificationCenter.default.addObserver(self, selector: #selector(myUIKitFunction), name: .executeUIKitFunction, object: nil)
Place that in your init or viewDidLoad, depending on what kind of representable.
Again, this is not "pure" SwiftUI or Combine, but someone better than me can probably give you that - and you sound willing to get something that works. And trust me, this works.
EDIT: Of note, you need to do nothing extra in your representable - this simply works between your model and your UIKit view or view controller.
I was coming here to find a better answer, then the one I came up myself with, but maybe this does actually help someone?
It's pretty verbose though nevertheless and doesn't quite feel like the most idiomatic solution, so probably not exactly what the question author was looking for. But it does avoid polluting the global namespace and allows synchronous (and repeated) execution and returning values, unlike the NotificationCenter-based solution posted before.
An alternative considered was using a #StateObject instead, but I need to support iOS 13 currently where this is not available yet.
Excursion: Why would I want that? I need to handle a touch event, but I'm competing with another gesture defined in the SwiftUI world, which would take precedence over my UITapGestureRecognizer. (I hope this helps by giving some context for the brief sample code below.)
So what I came up with, was the following:
Add an optional closure as state (on FooView),
Pass it as a binding into the view representable (BarViewRepresentable),
Fill this from makeUIView,
So that this can call a method on BazUIView.
Note: It causes an undesired / unnecessary subsequent update of BarViewRepresentable, because setting the binding changes the state of the view representable though, but this is not really a problem in my case.
struct FooView: View {
#State private var closure: ((CGPoint) -> ())?
var body: some View {
BarViewRepresentable(closure: $closure)
.dragGesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onEnded { value in
self.closure?(value.location)
})
)
}
}
class BarViewRepresentable: UIViewRepresentable {
#Binding var closure: ((CGPoint) -> ())?
func makeUIView(context: UIViewRepresentableContext<BarViewRepresentable>) -> BazUIView {
let view = BazUIView(frame: .zero)
updateUIView(view: view, context: context)
return view
}
func updateUIView(view: BazUIView, context: UIViewRepresentableContext<BarViewRepresentable>) {
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else { return }
strongSelf.closure = { [weak view] point in
guard let strongView = view? else {
return
}
strongView.handleTap(at: point)
}
}
}
}
class BazUIView: UIView { /*...*/ }
This is how I accomplished it succesfully. I create the UIView as a constant property in the SwiftUI View. Then I pass that reference into the UIViewRepresentable initializer which I use inside the makeUI method. Then I can call any method (maybe in an extension to the UIView) from the SwiftUI View (for instance, when tapping a button). In code is something like:
SwiftUI View
struct MySwiftUIView: View {
let myUIView = MKMapView(...) // Whatever initializer you use
var body: some View {
VStack {
MyUIView(myUIView: myUIView)
Button(action: { myUIView.buttonTapped() }) {
Text("Call buttonTapped")
}
}
}
}
UIView
struct MyUIView: UIViewRepresentable {
let myUIView: MKMapView
func makeUIView(context: UIViewRepresentableContext<MyUIView>) -> MKMapView {
// Configure myUIView
return myUIView
}
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MyUIView>) {
}
}
extension MKMapView {
func buttonTapped() {
print("The button was tapped!")
}
}

Am I using the State Pattern correctly?

I'm learning the State Pattern (Finite State Machine)
In the sample project that I built the only way that I figured out to update the UI is to pass a reference of the presenting view to the state machine, and then update the UI from the state I'm working. Am I doing it wrong?😕
Here's my State Machine
class CapturePhotoStateMachine {
var noPictureTakenState: NoPictureTakenState?
var pictureTakenState: PictureTakenState?
var initialState: InitialState?
var vc: SignupAvatarView?
var capturePhotoState: CapturePhotoState?
init(viewController: SignupAvatarView) {
noPictureTakenState = NoPictureTakenState(stateMachine: self)
pictureTakenState = PictureTakenState(stateMachine: self)
initialState = InitialState(stateMachine: self)
vc = viewController
capturePhotoState = initialState
}
func setCapturePhotoState(newState: CapturePhotoState) {
self.capturePhotoState = newState
}
func takePicture() {
self.capturePhotoState?.takePicture()
}
func savePicture(image: UIImage) {
self.capturePhotoState?.savePicture(image: image)
}
func retakePicture() {
self.capturePhotoState?.retakePicture()
}
func setup() {
self.capturePhotoState?.setup()
}
}
Here's my protocol
protocol CapturePhotoState {
func takePicture()
func savePicture(image: UIImage)
func retakePicture()
func setup()
}
Here's a subclass of state
class NoPictureTakenState: CapturePhotoState {
var stateMachine: CapturePhotoStateMachine?
init(stateMachine: CapturePhotoStateMachine) {
self.stateMachine = stateMachine
}
func takePicture() {
stateMachine!.vc?.previewView.isHidden = true
stateMachine!.vc?.capturedImage.isHidden = false
stateMachine!.vc?.saveButton.isHidden = false
stateMachine!.vc?.retakePhoto.isHidden = false
stateMachine?.setCapturePhotoState(newState: (stateMachine?.pictureTakenState)!)
}
func savePicture(image: UIImage) {
}
func retakePicture() {}
func setup() {}
}
The key to your state machine's purpose seems to be that you have interface objects you want to enable or disable depending on the state. That enablement / disablement should be the job of the view controller. The state itself is simply the basis on which questions can be answered such as "What is the current situation" and "What should happen next".
Here's a short simple state machine example that illustrates. It is deliberately trivial. We have just two buttons, and just two states; in each state, exactly one button should be enabled. The states are represented by cases of an enum, and we use a setter observer on that enum to respond whenever the state changes. The enum encapsulates the logic of how many states there are and what the next state is, while the view controller mediates between state change and interface change:
class ViewController: UIViewController {
#IBOutlet weak var takePictureButton: UIButton!
#IBOutlet weak var deletePictureButton: UIButton!
#IBOutlet weak var pictureImageView: UIImageView! // not used in the example
#IBAction func doTakePicture(_ sender: Any) {
// do stuff
doNextState()
}
#IBAction func doDeletePicture(_ sender: Any) {
// do stuff
doNextState()
}
enum State {
case pictureNotTaken
case pictureTaken
var nextState : State {
switch self {
case .pictureNotTaken:
return .pictureTaken
case .pictureTaken:
return .pictureNotTaken
}
}
}
var state : State = .pictureNotTaken {
didSet {
updateInterface()
}
}
func doNextState() {
self.state = self.state.nextState // triggers the setter observer
}
func updateInterface() {
switch state {
case .pictureNotTaken:
takePictureButton.isEnabled = true
deletePictureButton.isEnabled = false
case .pictureTaken:
takePictureButton.isEnabled = false
deletePictureButton.isEnabled = true
}
}
}
Probably what you want is some expansion of that pattern.
the only way that I figured out to update the UI is to pass a reference of the presenting view to the state machine
That is what the above pattern does not do. The setter observer solves that problem for us.
Now, you might object that the switch statement in updateInterface is doing the wrong kind of work. It locates the knowledge of how the interface reflects the state entirely in the view controller. Your impulse is to say that surely that knowledge is part of the state (and that's why you constructed your code the way you did).
My reply would be: well, yes and no. I do sometimes feel that way, and the way I solve the problem is to endow the state machine with properties expressing all the questions the view controller might have about what the current state means as regards the interface. That way, the knowledge is moved to the state, but the interface is still governed, correctly, by the view controller.
So, for example, we might add these two properties to our State enum:
enum State {
// ... everything else is as before ...
var userCanTakePicture : Bool { return self == .pictureNotTaken }
var userCanDeletePicture : Bool { return self == .pictureTaken }
}
So, now, our updateInterface doesn't need any special knowledge about what each state means; it merely asks the state what the interface should be, which is simpler and gives perhaps a more satisfying separation of powers:
func updateInterface() {
self.takePictureButton.isEnabled = state.userCanTakePicture
self.deletePictureButton.isEnabled = state.userCanDeletePicture
}