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

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.

Related

Core Data with SwiftUI MVVM Feedback

I am looking for a way to use CoreData Objects using MVVM (ditching #FetchRequest). After experimenting, I have arrived at the following implementation:
Package URL: https://github.com/TimmysApp/DataStruct
Datable.swift:
protocol Datable {
associatedtype Object: NSManagedObject
//MARK: - Mapping
static func map(from object: Object) -> Self
func map(from object: Object) -> Self
//MARK: - Entity
var object: Object {get}
//MARK: - Fetching
static var modelData: ModelData<Self> {get}
//MARK: - Writing
func save()
}
extension Datable {
static var modelData: ModelData<Self> {
return ModelData()
}
func map(from object: Object) -> Self {
return Self.map(from: object)
}
func save() {
_ = object
let viewContext = PersistenceController.shared.container.viewContext
do {
try viewContext.save()
}catch {
print(String(describing: error))
}
}
}
extension Array {
func model<T: Datable>() -> [T] {
return self.map({T.map(from: $0 as! T.Object)})
}
}
ModelData.swift:
class ModelData<T: Datable>: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var publishedData = CurrentValueSubject<[T], Never>([])
private let fetchController: NSFetchedResultsController<NSFetchRequestResult>
override init() {
let fetchRequest = T.Object.fetchRequest()
fetchRequest.sortDescriptors = []
fetchController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: PersistenceController.shared.container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchController.delegate = self
do {
try fetchController.performFetch()
publishedData.value = (fetchController.fetchedObjects as? [T.Object] ?? []).model()
}catch {
print(String(describing: error))
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let data = controller.fetchedObjects as? [T.Object] else {return}
self.publishedData.value = data.model()
}
}
Attempt.swift:
struct Attempt: Identifiable, Hashable {
var id: UUID?
var password: String
var timestamp: Date
var image: Data?
}
//MARK: - Datable
extension Attempt: Datable {
var object: AttemptData {
let viewContext = PersistenceController.shared.container.viewContext
let newAttemptData = AttemptData(context: viewContext)
newAttemptData.password = password
newAttemptData.timestamp = timestamp
newAttemptData.image = image
return newAttemptData
}
static func map(from object: AttemptData) -> Attempt {
return Attempt(id: object.aid ?? UUID(), password: object.password ?? "", timestamp: object.timestamp ?? Date(), image: object.image)
}
}
ViewModel.swift:
class HomeViewModel: BaseViewModel {
#Published var attempts = [Attempt]()
required init() {
super.init()
Attempt.modelData.publishedData.eraseToAnyPublisher()
.sink { [weak self] attempts in
self?.attempts = attempts
}.store(in: &cancellables)
}
}
So far this is working like a charm, however I wanted to check if this is the best way to do it, and improve it if possible. Please note that I have been using #FetchRequest with SwiftUI for over a year now and decided to move to MVVM since I am using it in all my Storyboard projects.
For a cutting edge way to wrap the NSFetchedResultsController in SwiftUI compatible code you might want to take a look at AsyncStream.
However, #FetchRequest currently is implemented as a DynamicProperty so if you did that too it would allow access the managed object context from the #Environment in the update func which is called on the DynamicProperty before body is called on the View. You can use an #StateObject internally as the FRC delegate.
Be careful with MVVM because it uses objects where as SwiftUI is designed to work with value types to eliminate the kinds of consistency bugs you can get with objects. See the doc Choosing Between Structures and Classes. If you build an MVVM object layer on top of SwiftUI you risk reintroducing those bugs. You're better off using the View data struct as it's designed and leave MVVM for when coding legacy view controllers. But to be perfectly honest, if you learn the child view controller pattern and understand the responder chain then there is really no need for MVVM view model objects at all.
And FYI, when using Combine's ObservableObject we don't sink the pipeline or use cancellables. Instead, assign the output of the pipeline to an #Published. However, if you aren't using CombineLatest, then perhaps reconsider if you should really be using Combine at all.

SwiftUI: Display file url from Array after user Picks file using DocumentPicker

I followed a tutorial on getting Url from a Document a user chooses and be able to display it on the View. My problem now is I want to add those Url's into an array. Then get the items from the array and print them onto the View. The way it works is the User presses a button and a sheet pops up with the files app. There the user is able to choose a document. After the user chooses the document the Url is printed on the View. To print the Url is use this
//if documentUrl has an Url show it on the view
If let url= documentUrl{
Text(url.absoluteString)
}
Issue with this is that when I do the same thing the
If let url= documentUrl
Is ran before the Url is even added to the array and the app crashes
Here is the full code
//Add the Urls to the array
class Article: ObservableObject{
var myArray:[String] = []
}
struct ContentView: View {
#State private var showDocumentPicker = false
#State private var documentUrl:URL?
#State var myString:URL?
#ObservedObject var userData:Article
// Func for onDismiss from the Sheet
func upload() {
// add the Url to the Array
DispatchQueue.main.async{
userData.myArray.append(documentUrl!.absoluteString)
}
}
var body: some View {
VStack{
//If have Url reflect that on the View
if let url = documentUrl{
//Works
Text(url.absoluteString)
//doesntwork
Text(userData.myArray[0])
}
}
Button(action:{showDocumentPicker.toggle()},
label: {
Text("Select your file")
})
.sheet(isPresented: $showDocumentPicker, onDismiss: upload )
{
DocumentPicker(url: $documentUrl)
}
}
}
The main thing I want to do the just display the ulrs into the view after the user chooses the document or after the sheet disappears. So if the user chooses 1 Url only one is printed. If another one is chosen after then 2 are show etc.
This is the documentPicker code used to choose a document
struct DocumentPicker : UIViewControllerRepresentable{
#Binding var url : URL?
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
//initialize a UI Document Picker
let viewController = UIDocumentPickerViewController(forOpeningContentTypes: [.epub])
viewController.delegate = context.coordinator
print("1")
return viewController
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {
print("Swift just updated ")
print("2")
}
}
extension DocumentPicker{
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator:NSObject, UIDocumentPickerDelegate{
let parent: DocumentPicker
init(_ documentPicker: DocumentPicker){
self.parent = documentPicker
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else{return}
parent.url = url
print("3")
}
}
}
Not sure if maybe I'm not approaching this the correct way? I looked at different tutorial but couldn't find anything.
Use .fileImporter presentation modifire (above ios 14)
.fileImporter(isPresented: $showDocumentPicker,
allowedContentTypes: [.image],
allowsMultipleSelection: true)
{ result in
// processing results Result<[URL], Error>
}
An observable object doesn't have a change trigger. To inform that the observable object has changed use one of the following examples:
class Article: ObservableObject {
#Published var myArray:[String] = []
}
or
class Article: ObservableObject {
private(set) var myArray:[String] = [] {
willSet {
objectWillChange.send()
}
}
func addUrl(url: String) {
myArray.append(url)
}
}
official documentation: https://developer.apple.com/documentation/combine/observableobject

How can I get #AppStorage to work in an MVVM / SwiftUI framework?

I have a SettingsManager singleton for my entire app that holds a bunch of user settings. And I've got several ViewModels that reference and can edit the SettingsManager.
The app basically looks like this...
import PlaygroundSupport
import Combine
import SwiftUI
class SettingsManager: ObservableObject {
static let shared = SettingsManager()
#AppStorage("COUNT") var count = 10
}
class ViewModel: ObservableObject {
#Published var settings = SettingsManager.shared
func plus1() {
settings.count += 1
objectWillChange.send()
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(action: viewModel.plus1) {
Text("\(viewModel.settings.count)")
}
}
}
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController
Frustratingly, it works about 85% of the time. But 15% of the time, the values don't update until navigating away from the view and then back.
How can I get #AppStorage to play nice with my View Model / MVVM framework?!
Came across this question researching this exact issue. I came down on the side of letting SwiftUI do the heavy lifting for me. For example:
// Use this in any view model you need to update the value
extension UserDefaults {
static func setAwesomeValue(with value: Int) {
UserDefaults.standard.set(value, forKey: "awesomeValue")
}
static func getAwesomeValue() -> Int {
return UserDefaults.standard.bool(forKey: "awesomeValue")
}
}
// In any view you need this value
struct CouldBeAnyView: some View {
#AppStorage("awesomeValue") var awesomeValue = 0
}
AppStorage is just a wrapper for UserDefaults. Whenever the view model updates the value of "awesomeValue", AppStorage will automatically pick it up. The important thing is to pass the same key when declaring #AppStorage. Probably shouldn't use a string literal but a constant would be easier to keep track of?
This SettingsManager in a cancellables set solution adapted from the Open Source ACHN App:
import PlaygroundSupport
import Combine
import SwiftUI
class SettingsManager: ObservableObject {
static let shared = SettingsManager()
#AppStorage("COUNT") var count = 10 {
willSet { objectWillChange.send() }
}
}
class ViewModel: ObservableObject {
#Published var settings = SettingsManager.shared
var cancellables = Set<AnyCancellable>()
init() {
settings.objectWillChange
.sink { [weak self] _ in
self?.objectWillChange.send()
}
.store(in: &cancellables)
}
func plus1() {
settings.count += 1
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(action: viewModel.plus1) {
Text(" \(viewModel.settings.count) ")
}
}
}
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController
Seems to be slightly less glitchy, but still isn't 100% rock-solid consistent :(
Leaving this here to hopefully inspire someone with my attempt

swiftui Properties binding to each other and swiftui

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

SwiftUI and RxSwift Observer Closure Behavior

I'm building an iOS app, using RxSwift and SwiftUI. I'm completely new to these frameworks so I was following a few tutorials, but I'm having a hard time figuring how to setup a Observer coupled with SwiftUI whereas I'd like to keep updating my UI as long as my BehaviorRelay list of events is updated, here's what I've got in my UI:
import SwiftUI
import RxSwift
struct EventsTableView: View {
private let observer: EventsTableObserver = EventsTableObserver()
init() {
observer.setObserver()
EventViewModel.getAllEvents()
}
var body: some View {
List{
ForEach(observer.events_view,id: \.id) { event in
HStack {
Text(event.title)
}
}
}
}
}
class EventsTableObserver {
private let disposeBag = DisposeBag()
var events_view = [Event]()
func setObserver(){
EventGroup.shared.events.asObservable()
.subscribe(onNext: {
[unowned self] events in
self.events_view = events
})
.disposed(by: disposeBag)
}
}
The problem is that apparently after my closure ends, self.events_view is not keeping the stored events values as I'd like to, even though the events are being updated. Can someone give me a direction here?