How to trigger a function via variable change (binding) - swiftui

I have a timer view which I want to reuse and I want to start the timer by my binding variable running like this:
Unfortunately I am (too?) tired this morning to find a solution how to do that. :(
Maybe my solution is bad and there is a much easier way!?
struct TimerView: View {
#State var hours : Int = 0
#State var minutes : Int = 0
#State var seconds : Int = 0
#State var hundred : Int = 0
#Binding var running : Bool
let timer = Timer.publish (every: 0.01, on: .main, in: .common)
#State var cancellable : Cancellable?
func start() {
self.cancellable = timer.connect()
}
func stop() {
cancellable!.cancel()
}

You can do the following, create this extension
extension Binding {
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
let snapshot = self.wrappedValue
self.wrappedValue = $0
execute(snapshot)
}
)
}
}
And the the parent view, not TimerView you can have something like this
//[...]
TimerView(running: $running.didSet{ value in /* your code or function here*/ })
//[...]
Check my answer here
UPDATE 1
Based on the comment what I understood is that the you want to start/stop the timer from outside the TimerView.
I found a couple ways to do that
First one declare the TimerView as a variable in the parent view
struct ContentView: View {
#State var running: Bool = false
#State var timerView: TimerView? = nil
init() {
timerView = TimerView(running: $running)
}
// rest...
}
But depending on your init it can be a pain so I created a small extension that allows you to reference the view like so
extension View {
func `referenced`<T>(by reference: Binding<T>) -> (Self?){
DispatchQueue.main.async { reference.wrappedValue = self as! T }
return self
}
}
The usage of this extension is rather simple
struct ContentView: View {
#State var running: Bool = false
#State var timerView: TimerView? = nil
var body: some View {
TimerView(running: $running).referenced(by: $timerView)
}
}
Then you can control it using for instance a button Button(action: self.timerView.start()) { Text("Start Timer") }
If tested this code against your TimerView but seems there's something wrong in the stop() code. I commented the line cancellable!.cancel() and added a print instead and everything is working as expected.
Here's the test code that I used in Xcode Playground
I hope this is the answer you're looking for.

Related

#Published value don't pass through views

I'm beggining with SwiftUI and I wanted to develop a small simple app to practice. I have a problem with #Published property that don't pass through views and so don't update the view.
I explain : In the first view I calculate the vMoyenne property and update it. I wanted to show this value in the next view ("Passage") to be able to use it for some other calculation but I tried many thing and the value in the "Passage" View doesn't update...
Here is the code :
ContentView.swift :
struct ContentView: View {
var body: some View {
TabView {
SpeedView().tabItem {
Label("Vitesse", systemImage: "figure.run.circle.fill")
}
PassageView(parameters: Parameters()).tabItem {
Label("Passage", systemImage: "timer.circle.fill")
}
}
}
}
Parameters.swift
class Parameters: ObservableObject {
#Published var distance: Double?
static let units = ["m", "km"]
#Published var unit = 1
#Published var hour: Int = 0
#Published var minute: Int = 0
#Published var second: Int = 0
#Published var vMoyenne = 0.0
#Published var allure = 0.0
#Published var convertedDecimalToSeconds = 0
var time: Int?
...
func calcVMoy() -> Void{
var d = distance!
let t = Double(time!) / 3600
var unite: String {
return Parameters.units[unit]
}
var calc = 0.0
if unite == "km" {
calc = d / t
} else {
d = d / 1000
calc = d / t
}
vMoyenne = calc
}
...
init() {
}
}
**SpeedView.swift **
struct SpeedView: View {
#ObservedObject var parameters = Parameters()
...
...
Button {
showVMoy = true
disableChange = true
if parameters.distance == nil {
parameters.distance = 0
} else {
parameters.runCalc()
}
} label: {
Text("Calculer")
}
... *// Here I can show and see the calculated vMoyenne property without problem...*
...
}
And the PassageView.swift where I want to show the vMoyenne property...
struct PassageView: View {
#ObservedObject var parameters:Parameters
var body: some View {
Text("\(parameters.vMoyenne)") *//want to show the vMoyenne value that we calculate previously but it always show 0,000...*
}
}
Thanks a lot for your help !!
PS : I tried many things like using didSet but I don't understand what I did wrong...
I found some post on stackoverflow but when I tried it doesn't work...
If you update the ContentView to it should work. The problem was that the SpeedView and PassageView were not sharing the same parameters object
struct ContentView: View {
#StateObject var parameters: Parameters = .init()
var body: some View {
TabView {
SpeedView(parameters: parameters).tabItem {
Label("Vitesse", systemImage: "figure.run.circle.fill")
}
PassageView(parameters: parameters).tabItem {
Label("Passage", systemImage: "timer.circle.fill")
}
}
}
}

SwiftUI Combine Why can't bind data in init?

I am trying a very simple test to just combine a simple Just("JustValue") to a property.
But it did not work.
↓ This is my code
struct ResponseView: View {
private var contentCancellable: AnyCancellable? = nil
#State var content: String = "InitialValue"
var body: some View {
Text(content)
}
init() {
contentCancellable = Just("JustValue").assign(to: \.content, on: self)
}
}
Is there anyone know why the Text shows "InitialValue" instead "JustValue"
This is specific of state property wrapper initialization pass... the external state storage is created later so only one initialisation is applied.
If you want to update it, do it later, when state be already created and linked to view, like
struct ResponseView: View {
#State var content: String = "InitialValue"
var body: some View {
Text(content)
.onAppear {
_ = Just("JustValue").assign(to: \.content, on: self)
}
}
}
the gives UI which you expected.

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.

Custom event modifier in SwiftUI

I created a custom button, that shows a popover. Here is my code:
PopupPicker
struct PopupPicker: View {
#State var selectedRow: UUID?
#State private var showPopover = false
let elements: [PopupElement]
var body: some View {
Button((selectedRow != nil) ? (elements.first { $0.id == selectedRow! }!.text) : elements[0].text) {
self.showPopover = true
}
.popover(isPresented: self.$showPopover) {
PopupSelectionView(elements: self.elements, selectedRow: self.$selectedRow)
}
}
}
PopupSelectionView
struct PopupSelectionView: View {
var elements: [PopupElement]
#Binding var selectedRow: UUID?
var body: some View {
List {
ForEach(self.elements) { element in
PopupText(element: element, selectedRow: self.$selectedRow)
}
}
}
}
PopupText
struct PopupText: View {
var element: PopupElement
#Binding var selectedRow: UUID?
var body: some View {
Button(element.text) {
self.presentation.wrappedValue.dismiss()
self.selectedRow = self.element.id
}
}
}
That works fine, but can I create a custom event modifier, so that I can write:
PopupPicker(...)
.onSelection { popupElement in
...
}
I can't give you a full solution as I don't have all of your code and thus your methods to get the selected item anyhow, however I do know where to start.
As it turns out, declaring a function with the following syntax:
func `onSelection`'(arg:type) {
...
}
Creates the functionality of a .onSelection like so:
struct PopupPicker: View {
#Binding var selectedRow: PopupElement?
var body: some View {
...
}
func `onSelection`(task: (_ selectedRow: PopupElement) -> Void) -> some View {
print("on")
if self.selectedRow != nil {
task(selectedRow.self as! PopupElement)
return AnyView(self)
}
return AnyView(self)
}
}
You could theoretically use this in a view like so:
struct ContentView: View {
#State var popupEl:PopupElement?
var body: some View {
PopupPicker(selectedRow: $popupEl)
.onSelection { element in
print(element.name)
}
}
}
However I couldn't test it properly, please comment on your findings
Hope this could give you some insight in the workings of this, sorry if I couldn't give a full solution

ObservedObject not receiving changes in variable

I've got a Published variable that changes when a function is called. I print the variable within the class and I can see that it has set the variable but when it doesn't change my View, and when I print the variable it's only printing what I initially set the variable at.
I know there have been a couple similar posts on this, but after going through them I'm still not sure what I'm doing wrong.
Code is as follows: (minus bits I think aren't relevant here)
Class containing function that is called to change the variable
class MusicManager: NSObject, ObservableObject {
#Published var playlistLabel: String = "N/A"
func playPlaylistNow(chosenPlaylist: String?) {
...
playlistLabel = chosenPlaylist!
print(playlistLabel) // I get the revised variable printed here
}
}
View to Update
struct HomeView: View {
...
#ObservedObject var musicManager: MusicManager = MusicManager()
var body: some View {
...
SongLabels(trackLabel: currentTrack!, artistLabel: currentArtist!, playlistLabel: musicManager.playlistLabel)
...
}
...
.onAppear {
self.updateTrackData()
}
}
func updateTrackData() {
print("Playlist: \(musicManager.playlistLabel)") // I get the original "N/A" printed here
}
View that calls the function:
{
#State private var showingAlert = false
let musicManager: MusicManager = MusicManager()
var playlistName: String
var body: some View {
Button(action: {
self.showingAlert = true
self.musicManager.playPlaylistNext(chosenPlaylist: self.playlistName)
}) {
Text("Play Next")
}
.alert(isPresented: $showingAlert) {
...
}
...
}
}
You have more than one instance of MusicManager in your app, so they are not identical.
If you would like to print, you can print both musicManager , to see the difference.