SwiftUI Using on Receive with #FetchRequest Publisher - swiftui

I try to observe the changes in #FetchRequest publisher by using onReceive as shown in the code bellow, It's work but I have on issue I want to fix it, the publisher publish to many values and I just want to receive one value to know the change in #FetchRequest is happened.
Example Code
struct TodayView: View {
#FetchRequest(fetchRequest: TodoTaskManager.allOverdueTasksFetchRequest)
private var overdueTasks: FetchedResults<TodoTask>
// Some views...
.onReceive(overdueTasks.publisher.count()) { _ in
print("tasks count: \(overdueTasks.count)") // The print times equals tasks count x 2
}
}

Related

How does the pattern of assigning `#State` in `onReceive` ensure that view state is in sync?

I'm having a little trouble with the following pattern which integrates Combine publishers into SwiftUI so that view state is updated when publishers emit:
struct ItemList: View {
var publisher: AnyPublisher<[Item], Never>
#State private var items = [Item]()
var body: some View {
List(items) { item in
ItemRow(item: item)
}
.onReceive(publisher) {
items = $0
}
}
}
Above example from Swift by Sundell
I feel like I'm missing something when I read it.
Let's assume you initialize items to the correct (at that time) value. What ensures that the published value won't change between the creation of ItemList and the first call to body, where it first starts listening to changes? Or if there is no such guarantee, then what else is preventing the view from ending up in the wrong initial state because of this?
Consider a NavigationLink:
NavigationLink(
destination: { ItemList(publisher: myPub) },
label: { Text("Show List") }
)
Here we have a case where SwiftUI creates the ItemList immediately, but doesn't ask the ItemList for its body until the user taps the link.
(How do we know it creates the ItemList immediately? The destination argument is not declared #escaping, so SwiftUI has to call it inside the NavigationLink initializer.)
So in fact there is a real risk in this case that items should change between when the ItemList is created and when it appears on screen.
We solve this by using a publisher like CurrentValueSubject that publishes its current value immediately to each new subscriber. That way, it doesn't matter how much later SwiftUI decides to use the view. As soon as SwiftUI uses the view, it subscribes to the publisher and immediately gets the current value. SwiftUI can handle that update before updating the framebuffer, so the user doesn't see a flash of incorrect data.
We need to read it in sequence:
State is initiailzed, supposing items = [Item1, Item2, Item3]
body is called to render view
List is constructed with current items, ie. List([Item1, Item2, Item3])
onReceive is called on constructed List of 3) and creates view around that list with subscriber to publisher
subscriber requests current events from publisher
if there are events in publisher then onReceive's closure handler is called (see below) otherwise no changes and List of 3) is shown on screen
6.1. if handler gets same initial [Item1, Item2, Item3] (subscriber extracts all available items) then state is not changed and List of 3) is shown on screen
6.2. if handler gets different items [ItemX, ItemY] then state change invalidates view and List is rebuilt with [ItemX, ItemY] which are shown on screen (there is no cycling because refresh is synchronous and we get into 6.1 at second pass).
That's simplified logic of provided code snapshot.

SwiftUI changing environment object re-created observed object in same view

So, I have few steps, last one contains EnvironmentObject and ObservedObject. The issue is, when I try to modify EnvironmentObject (lane 68) it re-creates ObservedObject.
Can any one explain me why this happens? Any solution to keep my ObservedObject with original state?
As far as I know it possible to change ObservedObject to StateObject, but I am using iOS 13+ so... I need other solution.
Line 47 - body is reevaluated so new instance of ObservedStuff is created, so make it as property and pass it in, like
struct TestView_A: View {
...
private let model = ObservedStuff()
var body: some View {
NavigationLink(destination: TestView_B(viewModel: self.model) ...
}
}

SwiftUI change output format of `Text` using as `.timer`

Is there a way to change the output format of a Text using init(_ date: Date, style: Text.DateStyle)?
Using a .timer, the output is like: 0:42, but I want something like 00:00:42.
Background
I want to create a widget (iOS 14) where a timer is running, and as I think it's not a good idea to trigger a widget update every second, and this may even also not work reliably, at least that's not how widget are indented to be used.
So I thought about using this predefined timer functionality of Text.
I'm quite new to SwiftUI and don't really know about all the capabilities yet. So maybe I could create a similar custom Text-View by myself? Any ideas on how to achieve this?
Or asked differently: Is there a way to create such self-updating components by oneself, that also work in an iOS 14 widget? (Seems like using Timer.publish to update the View does not work in an iOS 14 widget)
No solution => Workaround
As of today, there doesn't seem to be a proper solution for this. But as the interest in a "solution" or workaround for this seems to be there, I'll just post what I ended up with, for now.
Code
Basically I just manually update the Text once a second, and calculate the difference from the reference date to "now".
struct SomeView: View {
let referenceDate = Date() // <- Put your start date here
#State var duration: Int = 0
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
var body: some View {
Text(self.duration.formatted(allowedUnits: [.hour, .minute, .second]) ?? "")
.onReceive(self.timer) { _ in
self.duration = referenceDate.durationToNow ?? 0
}
}
}
extension Date {
var durationToNow: Int? {
return Calendar.current
.dateComponents([.second], from: self, to: Date())
.second
}
}
extension Int {
func formatted(allowedUnits: NSCalendar.Unit = [.hour, .minute]) -> String? {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = allowedUnits
formatter.zeroFormattingBehavior = .pad
return formatter.string(from: DateComponents(second: self))
}
}
WidgetKit limitations
This unfortunately does not work for WidgetKit, as the timer does not run or at least the UI does not refresh. So I ended up, only displaying minutes in the widget (which is kinda ok for my purpose) and just set an update in the "timeline" for each minute.

SwiftUI TextField freezes when deleting first character

When specifying a minimumScaleFactor for a TextField in SwiftUI the TextField behaves normally while you enter text and reduces the font as specified when the content does not fit the TextView. However, if you start deleting characters everything works as usual until you delete the first character. Everything freezes.
At the beginning I though it was something in the way I was handling the variable that stores the text that in my application I have it as an ObservedObject. However, after debugging the frozen app I noticed that the code was circling around the drawing of the TextField over and over, function after function everything pointed to an error in the drawing of the object on the screen.
The following code illustrates the issue. The TextField works perfectly when you enter characters and delete them until you get to the first one. The it freezes.
import SwiftUI
struct ContentView: View {
#State var sensorNumber: String = ""
var body: some View {
TextField("WC0.000.000.000", text: $sensorNumber)
.padding(.all, 5.0)
.font(Font.custom("Helvetica", size:40.0))
.minimumScaleFactor(0.90)
}
}
The problem seems to be related to the interaction of the Custom Font. Obviously, my application is using custom fonts but here I just wanted to simplify the code.
This code does not fail if you don't use a custom font or if you don't specify a minimumScaleFactor. I have found a workaround that is not very elegant but it works until Apple fixes this bug:
import SwiftUI
struct ContentView: View {
#State var sensorNumber: String = ""
var body: some View {
TextField("WC0.000.000.000", text: $sensorNumber)
.padding(.all, 5.0)
.font(Font.custom("Helvetica", size:40.0))
.minimumScaleFactor(sensorNumber.count < 2 ? 1.0 : 0.90)
}
}
I am submitting a radar to Apple but looking for a better solution for the problem here.

Passing data to another view controller in swift 3

How do I send coredata values when selecting a row in table view to another view controller using SHOW SEGUE in swift 3?
This is my code in objective c using Present Modally Segue and I want the equivalent of that code in swift 3 using Show Segue because I'm using a navigation controller. I've been working on this for a couple hours but as of now, I still haven't been able to do it.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"UpdateDevice"])
{
NSManagedObject *selectedDevice = [self.accounts objectAtIndex:[[self.tableView indexPathForSelectedRow] row]];
detailViewController *destViewController = segue.destinationViewController;
destViewController.account = selectedDevice;
}
}