Instance method 'onReceive(_:perform:)' requires that 'CountdownTimer' conform to 'Publisher' - swiftui

I'm currently building out a project where a user could create multiple timers however I'm running into the following error. "Instance method 'onReceive(_:perform:)' requires that 'CountdownTimer' conform to 'Publisher'". I just can't figure out how to get Countdown Timer to conform to Publisher.
Here is the code for my timer object:
struct CountdownTimer: Identifiable {
let id = UUID()
let name: String
var minutes: Int
var seconds: Int
var countdown: Int {
let totalTime = seconds + (minutes * 60)
return totalTime
}
}
func timeString(time: Int) -> String {
return String(format: "%01i:%02i", minutes, seconds)
}
class CountdownTimers: ObservableObject {
#Published var timers = [CountdownTimer]()
}
My Content View:
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var countdownTimers = CountdownTimers()
#State private var showingAddSheet = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
Color.pastelGreen
.edgesIgnoringSafeArea(.all)
VStack {
ScrollView {
ForEach(countdownTimers.timers) { timer in
ZStack {
CardView()
.overlay(
HStack {
VStack(alignment: .leading) {
Text("\(timer.countdown)")
.font(.custom("Quicksand-Bold", size: 30))
Text(" \(timer.timeString(time: timer.countdown))")
.font(.custom("Quicksand-Bold", size: 40))
.onReceive(timer){ _ in
if timers.countdown > 0 {
timers.countdown -= 1
}
}
The error is happening on the .OnReceive(timer) line. the minutes and seconds are created on a different "AddView" but I don't believe that the issue is related to that section.
Any help would be greatly appreciated.

You're shadowing the name timer. It appears first here:
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
Then, later, you use it again here:
ForEach(countdownTimers.timers) { timer in
Swift is going to assume you always mean the timer in the most-recently-defined scope, so when you use .onReceive, it assumes you mean timer from the ForEach (which is of type CountdownTimer) and not the Timer defined earlier.
To fix this, use a different name in your ForEach and adjust your code (such as the Text elements) to use the new name.

Related

How do I make a currency calculations based on rounded times?

I have SwiftUI code which computes the time duration between two times (startTime and endTime) and rounds up to nearest 15 minutes. But how do I calculate the currency rate of $220 per hour from this duration?
I also seem to be struggling with organizing my code into view code (for SwiftUI) and also including the numerical code that runs in the background.
But here's my code I have so far with comments where I need to include this code.
import SwiftUI
struct ContentView: View {
#State private var startTime = Date().zeroSeconds
#State private var endTime = Date().zeroSeconds
#State private var number15Intervals = 0
#State private var amountDue = 0.0
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter Case Times:")) {
DatePicker("Start Time", selection: $startTime , displayedComponents: .hourAndMinute)
DatePicker("End Time", selection: $endTime, in: startTime..., displayedComponents: .hourAndMinute)
}
Section(header: Text("Case Duration:")) {
Text("duration = \(self.duration) min")
Text("duration (15m) = \(self.duration15) min")
}
Section(header: Text("Amount Due:")) {
// What code do I put here to calculate currency (US dollars)
// which equals time (rounded up by 15 min) times a rate of $220 per hour?
Text(amountDue, format: .currency(code: Locale.current.currencyCode ?? "USD"))
}
}
.navigationTitle("DDA Rates Calculator")
}
}
var duration: TimeInterval {
guard endTime > startTime else {
return 0
}
let dateIntervalMinutes = DateInterval(start: startTime, end: endTime).duration / 60
return dateIntervalMinutes
}
var duration15: TimeInterval {
return (self.duration/15.0).rounded(.up)*15
}
}
extension Date {
var zeroSeconds: Date {
let calendar = Calendar.current
let dateComponents = calendar.dateComponents([.hour, .minute], from: self)
return calendar.date(from: dateComponents) ?? self
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sorry if I'm not too clear here. I've tried all sorts of ways to do this but always seem to get all sorts of errors from XCode. I think it would be more confusing to show what I've tried so far since I tried it so many ways without success. I'm not understanding the scope and how to reference variables properly in SwiftUI.
You can replace your amountDue #State variable with a computed property:
var amountDue: Double {
duration15 / 60 * 220
}
(You can also remove the unused number15Intervals)

SwiftUI #ObservedObject updating view's state from the back-stack?

I'm implementing a Timer, to show an Alert after 10 seconds, using Combine's Publisher and #ObservedObject, #StateObject or #State to manage states in a screen A. The problem is when I navigate to screen B through a NavigationLink the Alert still show up.
Is there a way to process the state changes of a view only when it's on top?
struct NavigationView: View {
let timer = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.receive(on: DispatchQueue .main)
.scan(0) { counter, _ in
counter + 1
}
#State private var counter = "Seconds"
#State private var alert: AlertConfiguration?
var body: some View {
ZStack {
HStack(alignment: .top) {
Text(counterText)
Spacer()
}
NavigationLink(
destination: destinationView
) {
Button(Strings.globalDetails1) {
navigationAction()
}
}
}
.onReceive(timer) { count in
if count == 10 {
makeAlert()
}
setSeconds(with: count)
}
.setAlert(with: $alert) // This is just a custom ViewModifier to add an Alert to a view
}
private func makeAlert() {
alert = AlertConfiguration()
}
private func setSeconds(with count: Int) {
counter = "seconds_counter".pluralLocalization(count: count)
}
}
You could add a variable that you set true if your View is in foreground and will be set false if you switch to a different View. Then you only increase the timer if said variable is true
.onReceive(timer) {
if count == 10 {
makeAlert()
}
if (timerIsRunning) {
count += 1
}
}
So basically the solution is to have a publisher that supports the lifecycle of the screen (onAppear & onDisappear).
This Answer helped: SwiftUI Navigation Stack pops back on ObservableObject update

SwifUI onAppear gets called twice

Q1: Why are onAppears called twice?
Q2: Alternatively, where can I make my network call?
I have placed onAppears at a few different place in my code and they are all called twice. Ultimately, I'm trying to make a network call before displaying the next view so if you know of a way to do that without using onAppear, I'm all ears.
I have also tried to place and remove a ForEach inside my Lists and it doesn't change anything.
Xcode 12 Beta 3 -> Target iOs 14
CoreData enabled but not used yet
struct ChannelListView: View {
#EnvironmentObject var channelStore: ChannelStore
#State private var searchText = ""
#ObservedObject private var networking = Networking()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
.padding(.top, 20)
List() {
ForEach(channelStore.allChannels) { channel in
NavigationLink(destination: VideoListView(channel: channel)
.onAppear(perform: {
print("PREVIOUS VIEW ON APPEAR")
})) {
ChannelRowView(channel: channel)
}
}
.listStyle(GroupedListStyle())
}
.navigationTitle("Channels")
}
}
}
}
struct VideoListView: View {
#EnvironmentObject var videoStore: VideoStore
#EnvironmentObject var channelStore: ChannelStore
#ObservedObject private var networking = Networking()
var channel: Channel
var body: some View {
List(videoStore.allVideos) { video in
VideoRowView(video: video)
}
.onAppear(perform: {
print("LIST ON APPEAR")
})
.navigationTitle("Videos")
.navigationBarItems(trailing: Button(action: {
networking.getTopVideos(channelID: channel.channelId) { (videos) in
var videoIdArray = [String]()
videoStore.allVideos = videos
for video in videoStore.allVideos {
videoIdArray.append(video.videoID)
}
for (index, var video) in videoStore.allVideos.enumerated() {
networking.getViewCount(videoID: videoIdArray[index]) { (viewCount) in
video.viewCount = viewCount
videoStore.allVideos[index] = video
networking.setVideoThumbnail(video: video) { (image) in
video.thumbnailImage = image
videoStore.allVideos[index] = video
}
}
}
}
}) {
Text("Button")
})
.onAppear(perform: {
print("BOTTOM ON APPEAR")
})
}
}
I had the same exact issue.
What I did was the following:
struct ContentView: View {
#State var didAppear = false
#State var appearCount = 0
var body: some View {
Text("Appeared Count: \(appearrCount)"
.onAppear(perform: onLoad)
}
func onLoad() {
if !didAppear {
appearCount += 1
//This is where I loaded my coreData information into normal arrays
}
didAppear = true
}
}
This solves it by making sure only what's inside the the if conditional inside of onLoad() will run once.
Update: Someone on the Apple Developer forums has filed a ticket and Apple is aware of the issue. My solution is a temporary hack until Apple addresses the problem.
I've been using something like this
import SwiftUI
struct OnFirstAppearModifier: ViewModifier {
let perform:() -> Void
#State private var firstTime: Bool = true
func body(content: Content) -> some View {
content
.onAppear{
if firstTime{
firstTime = false
self.perform()
}
}
}
}
extension View {
func onFirstAppear( perform: #escaping () -> Void ) -> some View {
return self.modifier(OnFirstAppearModifier(perform: perform))
}
}
and I use it instead of .onAppear()
.onFirstAppear{
self.vm.fetchData()
}
you can create a bool variable to check if first appear
struct VideoListView: View {
#State var firstAppear: Bool = true
var body: some View {
List {
Text("")
}
.onAppear(perform: {
if !self.firstAppear { return }
print("BOTTOM ON APPEAR")
self.firstAppear = false
})
}
}
Let us assume you are now designing a SwiftUI and your PM is also a physicist and philosopher. One day he tells you we should to unify UIView and UIViewController, like Quantum Mechanics and the Theory of Relativity. OK, you are like-minded with your leader, voting for "Simplicity is Tao", and create an atom named "View". Now you say: "View is everything, view is all". That sounds awesome and seems feasible. Well, you commit the code and tell the PM….
onAppear and onDisAppear exists in every view, but what you really need is a Page lifecycle callback. If you use onAppear like viewDidAppear, then you get two problems:
Being influenced by the parent, the child view will rebuild more than one time, causing onAppear to be called many times.
SwiftUI is closed source, but you should know this: view = f(view). So, onAppear will run to return a new View, which is why onAppear is called twice.
I want to tell you onAppear is right! You MUST CHANGE YOUR IDEAS. Don’t run lifecycle code in onAppear and onDisAppear! You should run that code in the "Behavior area". For example, in a button navigating to a new page.
You can create the first appear function for this bug
extension View {
/// Fix the SwiftUI bug for onAppear twice in subviews
/// - Parameters:
/// - perform: perform the action when appear
func onFirstAppear(perform: #escaping () -> Void) -> some View {
let kAppearAction = "appear_action"
let queue = OperationQueue.main
let delayOperation = BlockOperation {
Thread.sleep(forTimeInterval: 0.001)
}
let appearOperation = BlockOperation {
perform()
}
appearOperation.name = kAppearAction
appearOperation.addDependency(delayOperation)
return onAppear {
if !delayOperation.isFinished, !delayOperation.isExecuting {
queue.addOperation(delayOperation)
}
if !appearOperation.isFinished, !appearOperation.isExecuting {
queue.addOperation(appearOperation)
}
}
.onDisappear {
queue.operations
.first { $0.name == kAppearAction }?
.cancel()
}
}
}
For everyone still having this issue and using a NavigationView. Add this line to the root NavigationView() and it should fix the problem.
.navigationViewStyle(StackNavigationViewStyle())
From everything I have tried, this is the only thing that worked.
We don't have to do it on .onAppear(perform)
This can be done on init of View
In case someone else is in my boat, here is how I solved it for now:
struct ChannelListView: View {
#State private var searchText = ""
#State private var isNavLinkActive: Bool = false
#EnvironmentObject var channelStore: ChannelStore
#ObservedObject private var networking = Networking()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
.padding(.top, 20)
List(channelStore.allChannels) { channel in
ZStack {
NavigationLink(destination: VideoListView(channel: channel)) {
ChannelRowView(channel: channel)
}
HStack {
Spacer()
Button {
isNavLinkActive = true
// Place action/network call here
} label: {
Image(systemName: "arrow.right")
}
.foregroundColor(.gray)
}
}
.listStyle(GroupedListStyle())
}
.navigationTitle("Channels")
}
}
}
}
I've got this app:
#main
struct StoriesApp: App {
var body: some Scene {
WindowGroup {
TabView {
NavigationView {
StoriesView()
}
}
}
}
}
And here is my StoriesView:
// ISSUE
struct StoriesView: View {
#State var items: [Int] = []
var body: some View {
List {
ForEach(items, id: \.self) { id in
StoryCellView(id: id)
}
}
.onAppear(perform: onAppear)
}
private func onAppear() {
///////////////////////////////////
// Gets called 2 times on app start <--------
///////////////////////////////////
}
}
I've resolved the issue by measuring the diff time between onAppear() calls. According to my observations double calls of onAppear() happen between 0.02 and 0.45 seconds:
// SOLUTION
struct StoriesView: View {
#State var items: [Int] = []
#State private var didAppearTimeInterval: TimeInterval = 0
var body: some View {
List {
ForEach(items, id: \.self) { id in
StoryCellView(id: id)
}
}
.onAppear(perform: onAppear)
}
private func onAppear() {
if Date().timeIntervalSince1970 - didAppearTimeInterval > 0.5 {
///////////////////////////////////////
// Gets called only once in 0.5 seconds <-----------
///////////////////////////////////////
}
didAppearTimeInterval = Date().timeIntervalSince1970
}
}
In my case, I found that a few views up the hierarchy, .onAppear() (and .onDisappear()) was only being called once, as expected. I used that to post notifications that I listen to down in the views that need to take action on those events. It’s a gross hack, and I’ve verified that the bug is fixed in iOS 15b1, but Apple really needs to backport the fix.

Correct way to use a Timer to trigger NavigationView change in SwiftUI

I have an app where I would like the user to be able to start a timed task, and when time runs out, the navigation hierarchy should pop and bring the user back. I have code that ~works, but I don't like the code smell. Is this the right way to approach something like this?
class SimpleTimerManager: ObservableObject {
#Published var elapsedSeconds: Double = 0.0
private(set) var timer = Timer()
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) {_ in
self.elapsedSeconds += 0.01
}
}
func stop() {
timer.invalidate()
elapsedSeconds = 0.0
}
}
struct ContentView: View {
#ObservedObject var timerManager = SimpleTimerManager()
var body: some View {
NavigationView {
NavigationLink(destination: CountDownIntervalView(
timerManager: timerManager, length: 5.0
)) {
Text("Start the timer!")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct CountDownIntervalView: View {
#ObservedObject var timerManager: SimpleTimerManager
var length: Double
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var interval: Double {
let interval = length - self.timerManager.elapsedSeconds
if interval <= 0 {
self.mode.wrappedValue.dismiss()
self.timerManager.stop()
}
return interval
}
var body: some View {
VStack {
Text("Time remaining: \(String(format: "%.2f", interval))")
Button(action: {
self.mode.wrappedValue.dismiss()
self.timerManager.stop()
}) {
Text("Quit early!")
}
}
.navigationBarBackButtonHidden(true)
.onAppear(perform: {
self.timerManager.start()
})
}
}
I have a custom back button, which is something I'd like to preserve. My main concern is that it feels wrong to have code that stops the timer and pops the navigation inside a computed property. I'd prefer something like
Text("\(interval)").onReceive(timerManager.timer, perform: { _ in
if self.interval <= 0 {
self.mode.wrappedValue.dismiss()
self.timerManager.stop()
}
})
inside CountDownIntervalView, but this generates a compiler error - Unable to infer complex closure return type; add explicit type to disambiguate - and, to be honest, I'm not sure that approach makes sense either (attaching the code that conditionally pops the navigation to a piece of UI). What is the "best practices" way of approaching this problem?
Thanks for any thoughts.
Here is a solution. Tested with Xcode 11.4 / iOS 13.4
struct CountDownIntervalView: View {
#ObservedObject var timerManager: SimpleTimerManager
var length: Double
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var interval: Double {
length - self.timerManager.elapsedSeconds
}
var body: some View {
VStack {
Text("Time remaining: \(String(format: "%.2f", interval))")
.onReceive(timerManager.$elapsedSeconds) { _ in
if self.interval <= 0 {
self.mode.wrappedValue.dismiss()
self.timerManager.stop()
}
}
// ... other your code

SwiftUI Timer.publish causing whole screen to refresh

GIF of Entire Screen Refreshing
I am currently learning combine and MVVM. My problem is when I try to use a timer.publish, eventually I'm going to create a stop button, it causes the entire screen to refresh instead of the Text I have .onReceive.
I was hoping someone could provide me some insight on how I'm using publishers and observers incorrectly.
View:
import SwiftUI
import Combine
struct ApptCardView: View {
#ObservedObject var apptCardVM: ApptCardViewModel
#State var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text("\(currentDate)")
.onReceive(timer) { input in
self.currentDate = input
}
Picker("Seizure Type", selection: $apptCardVM.typeIndex) {
ForEach(0..<apptCardVM.typeChoice.count) {
Text(self.apptCardVM.typeChoice[$0])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
View Model:
import Foundation
import Combine
class ApptCardViewModel: ObservableObject, Identifiable {
#Published var appt: ApptEvent
#Published var typeChoice = ["Quick", "Long", "FullService"]
#Published var typeIndex: Int = 0
private var cancellables = Set<AnyCancellable>()
init(appt: ApptEvent) {
self.appt = appt
}
}
If you want to refresh only a part of body, then separate that part into dedicated subview, eg:
struct ApptCardView: View {
#ObservedObject var apptCardVM: ApptCardViewModel
var body: some View {
VStack {
CurrentDateView() // << here !!
Picker("Seizure Type", selection: $apptCardVM.typeIndex) {
ForEach(0..<apptCardVM.typeChoice.count) {
Text(self.apptCardVM.typeChoice[$0])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
struct CurrentDateView: View {
#State private var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(currentDate)")
.onReceive(timer) { input in
self.currentDate = input // << refresh only own body !!
}
}
}