ObservableObject bug or not - timer

I still have the problem when the count reaches 3, the reset function only stops it, but the count is not set to 0. I use the reset function with a button, it works perfectly. i would like to understand it and hope someone knows the reason for it?
import SwiftUI
import Combine
import Foundation
class WaitingTimerClass: ObservableObject {
#Published var waitingTimerCount: Int = 0
var waitingTimer = Timer()
func start() {
self.waitingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.waitingTimerCount += 1 }}
func stop() { waitingTimer.invalidate() }
func reset() { waitingTimerCount = 0; waitingTimer.invalidate() }
}
struct ContentView: View {
#ObservedObject var observed = WaitingTimerClass()
var body: some View {
VStack {
Text("\(self.observed.waitingTimerCount)")
.onAppear { self.observed.start() }
.onReceive(observed.$waitingTimerCount) { count in
guard count == 3 else {return}
self.observed.reset() // does not work
}
Button(action: {self.observed.start()}) {
Text("Start") }
Button(action: {self.observed.reset()}) { // works
Text("Reset") }
Button(action: {self.observed.stop()}) {
Text("Stop") }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

It is because reset changes property affecting UI during body construction, so ignored. It should be changed as below
func reset() {
waitingTimer.invalidate()
DispatchQueue.main.async {
self.waitingTimerCount = 0
}
}

Related

Problems with switching array elements

I have a class that has a calculated property, which is an array consisting of instances of the structure.
struct Team: Identifiable, Codable, Hashable {
var id = UUID()
var name : String
}
class TeamRow : ObservableObject {
#Published var teamsArray : [Team] = [] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(teamsArray) {
UserDefaults.standard.setValue(encoded, forKey: "Teams")
}
}
}
init() {
if let teams = UserDefaults.standard.data(forKey: "Teams") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([Team].self, from: teams) {
self.teamsArray = decoded
return
}
}
}
}
Also, I have a view, with the ability to add elements(teams) to this array using a sheet.
struct PlayersRow: View {
#ObservedObject var teams = TeamRow()
#State private var team = ""
#State private var showTeamAddSheet = false
var body: some View {
Form {
ForEach(teams.teamsArray) { team in
Text(team.name)
.font(.system(size: 20))
.padding(.horizontal, 110)
.padding(.vertical, 10)
}
}
.navigationBarTitle("Teams")
.navigationBarItems(trailing: Button(action: {
self.showTeamAddSheet = true
}) {
Image(systemName: "plus")
.foregroundColor(.black)
.font(.system(size: 30))
})
.sheet(isPresented: $showTeamAddSheet) {
AddPlayerView(teams: self.teams)
}
}
}
This is a sheet view.
struct AddPlayerView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var teams : TeamRow
#State private var team = ""
var body: some View {
NavigationView {
Form {
TextField("Add new team", text: $team)
}
.navigationBarItems(trailing: Button(action: {
let newTeam = Team(name: self.team)
self.teams.teamsArray.append(newTeam)
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Сохранить")
.font(.custom("coolJazz", size: 20))
.foregroundColor(.black)
}))
.navigationBarTitle("Add Team")
}
}
}
And I have a view where I need to output the array elements one by one, using a button, clicked on the button, the view screen displayed 0 element, clicked on the button, displayed first element, etc.
struct GameView: View {
#ObservedObject var teams = TeamRow()
//#State var index = 0
var body: some View {
VStack(spacing: 40) {
//Text(teams.teamsArray[index]) this isn't worked, return an error: Initializer 'init(_:)' requires that 'Team' conform to 'StringProtocol'
Button(action: {
}) {
Text("press it")
}
}
}
}
struct GameView_Previews: PreviewProvider {
static var previews: some View {
GameView().environmentObject(TeamRow())
}
}
if the array is set initially in the class, I have no problem displaying the elements on the screen by increasing the index, but how to solve this problem I do not know...
Can some one explain newbie?
Change your GameView Code to following:
struct GameView: View {
#ObservedObject var teams = TeamRow()
#State var index = 0
var body: some View {
VStack(spacing: 40) {
if teams.teamsArray.count > index {
Text(teams.teamsArray[index].name)
}
Button(action: {
index += 1
}) {
Text("press it")
}
}
}
}

#Published array in multiple ViewModels - SwiftUI

I have app with only two Views, both Views has own ViewModel. ViewA shows and manipulate objects from selectedNumbers array. ViewB owns all available objects(numbers) - in this view I want manipulate selectedNumbers array, that is used by ViewA.
I'm trying to find out, how to share these selectedNumbers array between these two ViewModels. I tried to use EnvironmentObject, StaticObject etc. But nothing works as I need. What approach should I use to achieve desired result. Thanks for help!
import SwiftUI
struct ViewA: View {
#ObservedObject var viewModel = ViewModelA()
var body: some View {
VStack {
Text("\(viewModel.number)")
.font(.largeTitle)
.padding()
.onTapGesture {
viewModel.showNext()
}
ViewB()
}
}
}
class ViewModelA: ObservableObject {
var numbers: [Int] = []
#Published var number: Int
var index = 0
init() {
number = numbers.isEmpty ? 0 : numbers[index]
}
func showNext() {
guard !numbers.isEmpty else { return }
if index < numbers.count - 1 {
index += 1
} else {
index = 0
}
number = numbers[index]
}
}
struct ViewB: View {
#ObservedObject var viewModel = ViewModelB()
var body: some View {
HStack {
ForEach(viewModel.numbers, id: \.self) { number in
Text("\(number)")
.foregroundColor(viewModel.selectedNumbers.contains(number) ? .red : .black)
.onTapGesture {
viewModel.updateSelection(number)
}
}
}
}
}
class ViewModelB: ObservableObject {
#Published var numbers: [Int] = []
#Published var selectedNumbers: [Int] = []
init() {
numbers.append(contentsOf: [1,2,3,4,5,6,7,8])
}
func updateSelection(_ number: Int) {
if selectedNumbers.contains(number) {
selectedNumbers.remove(number)
} else {
selectedNumbers.append(number)
}
}
}
extension Array where Element: Equatable {
mutating func remove(_ object: Element) {
guard let index = firstIndex(of: object) else {return}
remove(at: index)
}
}
You can still keep the logic separate, but you need to keep a single source of truth, and if you want to share data among views, you either need to pass Bindings or you can also share #ObservedObject among Subviews.
import SwiftUI
struct ViewA: View {
#ObservedObject var viewModel = ViewModelA(modelB: ViewModelB())
var body: some View {
VStack {
Text("\(viewModel.number)")
.font(.largeTitle)
.padding()
.onTapGesture {
viewModel.showNext()
}
ViewB(model: viewModel)
}
}
}
class ViewModelA: ObservableObject {
var numbers: [Int] = []
#Published var number: Int
#Published var modelB:ViewModelB
var index = 0
init(modelB:ViewModelB) {
self.modelB = modelB
number = numbers.isEmpty ? 0 : modelB.selectedNumbers[index]
}
func showNext() {
guard !modelB.selectedNumbers.isEmpty else { return }
if index < modelB.selectedNumbers.count - 1 {
index += 1
} else {
index = 0
}
number = modelB.selectedNumbers[index]
}
}
struct ViewB: View {
#ObservedObject var model : ViewModelA
var body: some View {
HStack {
ForEach(model.modelB.selectedNumbers, id: \.self) { number in
Text("\(number)")
.foregroundColor(model.modelB.selectedNumbers.contains(number) ? .red : .black)
.onTapGesture {
model.modelB.updateSelection(number)
}
}
}
}
}
struct ViewModelB {
var selectedNumbers: [Int] = []
init() {
selectedNumbers.append(contentsOf: [1,2,3,4,5,6,7,8])
}
mutating func updateSelection(_ number: Int) {
if selectedNumbers.contains(number) {
selectedNumbers.remove(number)
} else {
selectedNumbers.append(number)
}
}
}
extension Array where Element: Equatable {
mutating func remove(_ object: Element) {
guard let index = firstIndex(of: object) else {return}
remove(at: index)
}
}

Why is my SwiftUI JSON not parsing properly?

Disclaimer: Another very basic question below. I am trying to learn the basics of IOS development.
I'm currently trying to parse data from an API to a SwiftUI project and am not able to successfully do so.
The code goes as follows:
import SwiftUI
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
struct ContentView: View {
var poems = [Poem]()
var body: some View {
VStack {
if let poem = poems.first {
Button("Refresh") {getPoem()}
Text("\(poem.author): \(poem.title)").bold()
Divider()
ScrollView {
VStack {
ForEach(poem.lines, id: \.self) {
Text($0)
}
}
}
}
}
}
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The code does not build. The error thrown happens in the Func getPoem where "Cannot find 'self' in scope".
Any ideas? All help is appreciated.
I meant something like
class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
getPoem()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
struct PoemContentView: View {
#ObservedObject var fetch = FetchPoem()
var body: some View {
VStack {
Button("Get Next Poem") { fetch.getPoem() }
if let poem = fetch.poems.first {
Text("\(poem.author): \(poem.title)").bold()
Divider()
ScrollView {
VStack {
ForEach(poem.lines, id: \.self) {
Text($0)
}
}
}
} else {
Spacer()
}
}
}
}

SwiftUI: How to build a customizable timer countdown and its separate view

I saw many blogs/articles on how to build a timer with Swift UI. But I cannot figure out to get what I really want working.
The 2 main issues I am facing are:
1. My TimerView is rebuilt whenever its parent view is rebuilt due to states changing
2. I am not able to send parameters to my #ObservedObject TimeCountDown property (2 parameters: duration coming from another view, and an onEnded completion)
class Round: ObservableObject {
#Publishedvar items: [Item]
var duration: Double
init(items: [Item], duration: Double) {
self.items = items
self.duration = duration
}
}
Struct ParentView: View {
#ObservedObject var round: Round
#State private var isRoundTerminated: Bool = false
var body: Some View {
VStack {
if isRoundTerminated {
RoundEndView()
} else {
TimerView(duration: round.duration, onEnded: onTimerTerminated)
RoundPlayView(items: round.items)
}
}
}
}
struct TimerView: View {
#ObservedObject countdown = TimeCountDown()
var duration: Double
var onEnded: (() -> Void)?
///// I DO NOT KNOW HOW TO PROPAGATE THE onEnded completion TO countdown:TimeCountDown
var body: Some View {
Text("There are \(countdown.remainingTime) remaining secs")
.onAppend() {
timer.start(duration: duration)
/// MAYBE I COULD ADD THE onEnded WITHIN THE start() CALL?
}
}
}
class TimeCountDown : ObservableObject {
var timer : Timer!
#Published var remainingTime: Double = 60
var onEnded: (() -> Void)?
init(onEnded: #escaping (() -> Void)?) {
self.onEnded = onEnded
}
func start(duration: Double) {
self.timer?.invalidate()
self.remainingTime = duration
timer = Timer.scheduledTimer(timeInterval:1, target: self, selector:#selector(updateCountdown), userInfo: nil, repeats: true)
}
#objc private func updateCount() {
remainingTime -= 1
if remainingTime <= 0 {
killTimer()
self.onEnded?()
}
}
private func killTimer() {
timer?.invalidate()
timer = nil
}
}
However that does not work...
I also tried to implement the following TimerView:
struct CountdownView: View {
#State private var remainingTime: Int = 60
#Binding var countingDown: Bool
var onEnded: (() -> Void)?
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
init(durations: TimerDuration, onEnded: (() -> Void)?, start: Binding<Bool>) {
self.onEnded = onEnded
self._countingDown = start
self.remainingTime = durations.duration
}
var body: some View {
Text("Remaining \(remainingTime) secs")
.onReceive(timer) {_ in
if self.countingDown {
if self.remainingTime > 0 {
self.remainingTime -= 1
} else {
self.onTerminated()
}
}
}
}
func onTerminated() {
timer.upstream.connect().cancel()
self.remainingTime = 0
onEnded?()
}
}
However when the ParentView is rebuilt very often (due to modifications to round.items (#Published from Round:ObservableObject) the timer can be frozen.
#George_E, please find below my update code (Please note that I simplified the nextWord() func (a but useless here) but it does modify the round.remainingDeck which is a Published property within the ObervableObject Round. That triggers a rebuild of RoundPlayView and that freezes the timer):
import SwiftUI
import Combine
class Round: ObservableObject {
#Published var remainingDeck: [String] = ["Round#1", "Round#2", "Round#3", "Round4"]
var roundDuration: Int = 5
}
struct ContentView: View {
#ObservedObject var round = Round()
var body: Some View {
RoundPlayView(round: round)
}
}
struct RoundPlayView: View {
#ObservedObject var round: Round
var duration: Int {
return round.roundDuration
}
#State private var timerStart: Bool = true
#State private var isTerminated: Bool = false
init(round: Round) {
self.round = round
}
var body: some View {
return VStack {
if self.isTerminated {
EmptyView()
} else {
VStack {
CountdownView(duration: duration, onEnded: timerTerminated, start: $timerStart)
Button(action: { self.addWord() }) {
Text("Add words to the deck")
}
}
}
}
}
private func nextWord() {
round.remainingDeck.append("New Word")
}
private func timerTerminated() {
terminateRound()
}
private func terminateRound() {
self.isTerminated = true
}
}
and here is my TimerView code:
struct CountdownView: View {
#State private var remainingTime: Int = 60
#Binding var countingDown: Bool
var onEnded: (() -> Void)?
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
init(duration: Int, onEnded: (() -> Void)?, start: Binding<Bool>) {
self.onEnded = onEnded
self._countingDown = start
self.remainingTime = duration
}
var body: some View {
Text("Remaining \(remainingTime) secs")
.onReceive(timer) {_ in
if self.countingDown {
if self.remainingTime > 0 {
self.remainingTime -= 1
} else {
self.onTerminated()
}
}
}
}
func onTerminated() {
timer.upstream.connect().cancel()
self.remainingTime = 0
onEnded?()
}
}

How can I run the filter function only when the searchbar text is not empty?

I have a search button and a search bar in SwiftUi View.
i'm try to run the function filter() when the user type in the Searchbar.
If I use the button to trig the action it works, but I want to trig the action when user typing in the bar.
I have tried with if else but give me a warning.
Generic parameter 'FalseContent' could not be inferred
1. In call to function 'buildEither(first:)' (SwiftUI.ViewBuilder)
here my code
struct ContentView: View {
#ObservedObject var dm: DataManager
#State private var searchTerm : String = ""
#State var filteredAirports: [AirportModel] = []
var body: some View {
VStack {
SearchBar(text: $searchTerm)
if searchTerm == "" {
/// don't do anything
} else {
self.dm.filter2(valoreSearhed: self.searchTerm, arrayTosearh: self.dm.airportVector)
}
List {
ForEach(dm.appoggio) { valore in
Text(valore.aptICAO)
}
}
}
}
}
``
your problem is not your if/else because this works:
as you can see, the if/else is in there and no compiler warning...
Tipp: try to comment out all statements and then iterate by uncommenting and check when compiler warns you -> then you have your error
struct ContentView: View {
var list = ["1","2","3"]
#State private var searchTerm : String = ""
var body: some View {
VStack {
// SearchBar(text: $searchTerm)
if searchTerm == "" {
/// don't do anything
} else {
// self.dm.filter2(valoreSearhed: self.searchTerm, arrayTosearh: self.dm.airportVector)
}
List(list, id: \.self) { item in
Text(item)
// ForEach(dm.appoggio) { valore in
// Text(valore.aptICAO)
// }
}
}
}
}
You can do this by using a custom Binding that executes the filtering when the searchTerm changes:
SearchBar(text: $searchTerm.didSet(execute: { _ in self.updateSearchResults() }))
...
extension Binding {
/// Execute block when value is changed.
///
/// Example:
///
/// Slider(value: $amount.didSet { print($0) }, in: 0...10)
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
self.wrappedValue = $0
execute($0)
}
)
}
}

Resources