How to have ForEach in view updated when array values change with SwiftUI - swift

I have this code here for updating an array and a dictionary based on a timer:
class applicationManager: ObservableObject {
static var shared = applicationManager()
#Published var applicationsDict: [NSRunningApplication : Int] = [:]
#Published var keysArray: [NSRunningApplication] = []
}
class TimeOpenManager {
var secondsElapsed = 0.0
var timer = Timer()
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.secondsElapsed += 1
let applicationName = ws.frontmostApplication?.localizedName!
let applicationTest = ws.frontmostApplication
let appMan = applicationManager()
if let appData = applicationManager.shared.applicationsDict[applicationTest!] {
applicationManager.shared.applicationsDict[applicationTest!] = appData + 1
} else {
applicationManager.shared.applicationsDict[applicationTest!] = 1
}
applicationManager.shared.keysArray = Array(applicationManager.shared.applicationsDict.keys)
}
}
}
It works fine like this and the keysArray is updated with the applicationsDict keys when the timer is running. But even though keysArray is updated, the HStack ForEach in WeekView does not change when values are added.
I also have this view where keysArray is being loaded:
struct WeekView: View {
static let shared = WeekView()
var body: some View {
VStack {
HStack(spacing: 20) {
ForEach(0..<8) { day in
if day == weekday {
Text("\(currentDay)")
.frame(width: 50, height: 50, alignment: .center)
.font(.system(size: 36))
.background(Color.red)
.onTapGesture {
print(applicationManager.shared.keysArray)
print(applicationManager.shared.applicationsDict)
}
} else {
Text("\(currentDay + day - weekday)")
.frame(width: 50, height: 50, alignment: .center)
.font(.system(size: 36))
}
}
}
HStack {
ForEach(applicationManager.shared.keysArray, id: \.self) {dictValue in
Image(nsImage: dictValue.icon!)
.resizable()
.frame(width: 64, height: 64, alignment: .center)
}
}
}
}
}

As mentioned in the comments, applicationManager should be an ObservableObject with #Published properties.
Then, you need to tell your view that it should look for updates from this object. You can do that with the #ObservedObject property wrapper:
struct WeekView: View {
#ObservedObject private var manager = applicationManager.shared
And later:
ForEach(manager.keysArray) {
Replace anywhere you had applicatoinManager.shared with manager within the WeekView
Additional reading: https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-observedobject-to-manage-state-from-external-objects

Related

How to update and compare new data user entered to previous SwiftUI

I have a list of devices with radio buttons which is filled when user chooses special cell. When I click again to change mind - I have 2 active radio buttons and cannot delete previous one. Do you have any suggestions using Swift tools?
My list cell where radio button located
struct DeviceChoiceRow : View {
#StateObject var deviceModel = DeviceViewModel()
#State var checker = false
var devices : DeviceModel
var body: some View {
HStack {
HStack(spacing: 10) {
Image(devices.device_image)
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
Text(devices.device_name)
.font(.RegularFont(size: 15))
.foregroundColor(.black)
}
Spacer()
Button(action: {
deviceModel.current_device = devices.device_name
print(deviceModel.current_device)
checker = deviceModel.check_device(current_device_entered: devices.device_name)
}) {
ZStack {
Circle()
.fill(Color.main_blue)
.frame(width: 12, height: 12)
.opacity(checker ? 1 : 0)
Circle()
.strokeBorder(Color.medium_grey, lineWidth: 1)
.frame(width: 24, height: 24)
}
}
}
.padding(.leading, 21)
.padding(.trailing, 38)
}
}
Class with Published values and checker
class DeviceViewModel : ObservableObject {
#Published var devices : [DeviceModel]
#Published var checker : String = ""
#Published var current_device : String = ""
init() {
let devices = DeviceDataSet.devices
self.devices = devices
}
func check_device(current_device_entered: String) -> Bool {
return current_device == current_device_entered
}
}

SwiftUI View method called but output missing

I have the View AlphabetLetterDetail:
import SwiftUI
struct AlphabetLetterDetail: View {
var alphabetLetter: AlphabetLetter
var letterAnimView : LetterAnimationView
var body: some View {
VStack {
Button(action:
animateLetter
) {
Image(uiImage: UIImage(named: "alpha_be_1")!)
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 120.0)
}
letterAnimView
}.navigationBarTitle(Text(verbatim: alphabetLetter.name), displayMode: .inline)
}
func animateLetter(){
print("tapped")
letterAnimView.timerWrite()
}
}
containing the View letterAnimView of Type LetterAnimationView:
import SwiftUI
struct LetterAnimationView: View {
#State var Robot : String = ""
let LETTER =
["alpha_be_1_81",
"alpha_be_1_82",
"alpha_be_1_83",
"alpha_be_1_84",
"alpha_be_1_85",
"alpha_be_1_86",
"alpha_be_1_87",
"alpha_be_1_88",
"alpha_be_1_89",
"alpha_be_1_90",
"alpha_be_1"]
var body: some View {
VStack(alignment:.center){
Image(Robot)
.resizable()
.frame(width: 80, height: 160, alignment: .center)
.onAppear(perform: timerWrite)
}
}
func timerWrite(){
var index = 0
let _ = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) {(Timer) in
Robot = LETTER[index]
print("one frame")
index += 1
if (index > LETTER.count - 1){
Timer.invalidate()
}
}
}
}
This gives me a fine animation, as coded in func timerWrite() and performed by .onAppear(perform: timerWrite).
After commenting //.onAppear(perform: timerWrite) I try animating by clicking
Button(action: animateLetter)
but nothing happens.
Maybe I got two different instances of letterAnimView, if so why?
Can anybody of you competent guys intentify my mistake?
Regards - Klaus
You don't want to store Views, they are structs so they are copied. Instead, create an ObservableObject to encapsulate this functionality.
I created RobotModel here with other minor changes:
class RobotModel: ObservableObject {
private static let atlas = [
"alpha_be_1_81",
"alpha_be_1_82",
"alpha_be_1_83",
"alpha_be_1_84",
"alpha_be_1_85",
"alpha_be_1_86",
"alpha_be_1_87",
"alpha_be_1_88",
"alpha_be_1_89",
"alpha_be_1_90",
"alpha_be_1"
]
#Published private(set) var imageName: String
init() {
imageName = Self.atlas.last!
}
func timerWrite() {
var index = 0
let _ = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] timer in
guard let self = self else { return }
self.imageName = Self.atlas[index]
print("one frame")
index += 1
if index > Self.atlas.count - 1 {
timer.invalidate()
}
}
}
}
struct AlphabetLetterDetail: View {
#StateObject private var robot = RobotModel()
let alphabetLetter: AlphabetLetter
var body: some View {
VStack {
Button(action: animateLetter) {
Image(uiImage: UIImage(named: "alpha_be_1")!)
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 120.0)
}
LetterAnimationView(robot: robot)
}.navigationBarTitle(Text(verbatim: alphabetLetter.name), displayMode: .inline)
}
func animateLetter() {
print("tapped")
robot.timerWrite()
}
}
struct LetterAnimationView: View {
#ObservedObject var robot: RobotModel
var body: some View {
VStack(alignment:.center){
Image(robot.imageName)
.resizable()
.frame(width: 80, height: 160, alignment: .center)
.onAppear(perform: robot.timerWrite)
}
}
}

SwiftUI - using variable count within ForEach

I have a view created through a ForEach loop which needs to take a variable count within the ForEach itself i.e. I need the app to react to a dynamic count and change the UI accoridngly.
Here is the view I am trying to modify:
struct AnimatedTabSelector: View {
let buttonDimensions: CGFloat
#ObservedObject var tabBarViewModel: TabBarViewModel
var body: some View {
HStack {
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.red)
ForEach(1..<tabBarViewModel.activeFormIndex + 1) { _ in
Spacer().frame(maxWidth: buttonDimensions).frame(height: 20)
.background(Color.blue)
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.green)
}
Circle().frame(
width: buttonDimensions,
height: buttonDimensions)
.foregroundColor(
tabBarViewModel.activeForm.loginFormViewModel.colorScheme
)
ForEach(1..<tabBarViewModel.loginForms.count - tabBarViewModel.activeFormIndex) { _ in
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.red)
Spacer().frame(maxWidth: buttonDimensions).frame(height: 20)
.background(Color.blue)
}
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.gray)
}
}
}
And the viewModel I am observing:
class TabBarViewModel: ObservableObject, TabBarCompatible {
var loginForms: [LoginForm]
#Published var activeForm: LoginForm
#Published var activeFormIndex = 0
private var cancellables = Set<AnyCancellable>()
init(loginForms: [LoginForm]) {
self.loginForms = loginForms
self.activeForm = loginForms[0] /// First form is always active to begin
setUpPublisher()
}
func setUpPublisher() {
for i in 0..<loginForms.count {
loginForms[i].loginFormViewModel.$isActive.sink { isActive in
if isActive {
self.activeForm = self.loginForms[i]
self.activeFormIndex = i
}
}
.store(in: &cancellables)
}
}
}
And finally the loginFormViewModel:
class LoginFormViewModel: ObservableObject {
#Published var isActive: Bool
let name: String
let icon: Image
let colorScheme: Color
init(isActive: Bool = false, name: String, icon: Image, colorScheme: Color) {
self.isActive = isActive
self.name = name
self.icon = icon
self.colorScheme = colorScheme
}
}
Basically, a button on the login form itself sets its viewModel's isActive property to true. We listen for this in TabBarViewModel and set the activeFormIndex accordingly. This index is then used in the ForEach loop. Essentially, depending on the index selected, I need to generate more or less spacers in the AnimatedTabSelector view.
However, whilst the activeIndex variable is being correctly updated, the ForEach does not seem to react.
Update:
The AnimatedTabSelector is declared as part of this overall view:
struct TabIconsView: View {
struct Constants {
static let buttonDimensions: CGFloat = 50
static let buttonIconSize: CGFloat = 25
static let activeButtonColot = Color.white
static let disabledButtonColor = Color.init(white: 0.8)
struct Animation {
static let stiffness: CGFloat = 330
static let damping: CGFloat = 22
static let velocity: CGFloat = 7
}
}
#ObservedObject var tabBarViewModel: TabBarViewModel
var body: some View {
ZStack {
AnimatedTabSelector(
buttonDimensions: Constants.buttonDimensions,
tabBarViewModel: tabBarViewModel)
HStack {
Spacer()
ForEach(tabBarViewModel.loginForms) { loginForm in
Button(action: {
loginForm.loginFormViewModel.isActive = true
}) {
loginForm.loginFormViewModel.icon
.font(.system(size: Constants.buttonIconSize))
.foregroundColor(
tabBarViewModel.activeForm.id == loginForm.id ? Constants.activeButtonColot : Constants.disabledButtonColor
)
}
.frame(width: Constants.buttonDimensions, height: Constants.buttonDimensions)
Spacer()
}
}
}
.animation(Animation.interpolatingSpring(
stiffness: Constants.Animation.stiffness,
damping: Constants.Animation.damping,
initialVelocity: Constants.Animation.velocity)
)
}
}
UPDATE:
I tried another way by adding another published to the AnimatedTabSelector itself to check that values are indeed being updated accordingly. So at the end of the HStack in this view I added:
.onAppear {
tabBarViewModel.$activeFormIndex.sink { index in
self.preCircleSpacers = index + 1
self.postCircleSpacers = tabBarViewModel.loginForms.count - index
}
.store(in: &cancellables)
}
And of course I added the following variables to this view:
#State var preCircleSpacers = 1
#State var postCircleSpacers = 6
#State var cancellables = Set<AnyCancellable>()
Then in the ForEach loops I changed to:
ForEach(1..<preCircleSpacers)
and
ForEach(1..<postCircleSpacers)
respectively.
I added a break point in the new publisher declaration and it is indeed being updated with the expected figures. But the view is still failing to reflect the change in values
OK so I seem to have found a solution - what I am presuming is that ForEach containing a range does not update dynamically in the same way that ForEach containing an array of objects does.
So rather than:
ForEach(0..<tabBarViewModel.loginForms.count)
etc.
I changed it to:
ForEach(tabBarViewModel.loginForms.prefix(tabBarViewModel.activeFormIndex))
This way I still iterate the required number of times but the ForEach updates dynamically with the correct number of items in the array

Array of structs not updating in a view

I have an array of observed object, that contains an array of structs, that contain data. I would like to show it onscreen. This data is originally shown onscreen but the changes are not pushed whenever I make a change to an item in the array. I am even changing the property within the struct. I have tried it in my manager class aswell. I've done a bit of digging, changing my approach several times, but I can't get this working. I am very new to swiftui/swift as-well-as stack-overflow.
Full code:
struct GameView: View {
#State var value: CGFloat = 0
#ObservedObject var circles = GameCircles()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Color.primary
.frame(width: 450, height: 800)
self.circles.getViews()
.onReceive(timer) { _ in
self.circles.tick()
}
}
.edgesIgnoringSafeArea(.all)
.gesture(DragGesture(minimumDistance: 20)
.onChanged { gest in
self.value = gest.location.x
})
}
}
class GameCircles: ObservableObject {
#Published var circles: [GameCircle] = []
func getViews() -> some View {
ForEach(circles, id: \.id) { circle in
circle.makeView()
}
}
func tick() {
for circle in circles {
circle.tick()
print(circle.y)
}
circles.append(GameCircle(x: Int.random(in: -200...200), y: -200))
}
}
struct GameCircle: Identifiable {
#State var x: Int
#State var y: Int
let color = Color.random()
var id = UUID()
func tick() {
self.y += 1
}
func makeView() -> some View {
return ZStack {
Circle()
.frame(width: 40, height: 40)
.foregroundColor(color)
.animation(.default)
Text("\(Int(y))")
.foregroundColor(.black)
}
.offset(x: CGFloat(self.x), y: CGFloat(self.y))
}
}
I played with this code around, trying to solve your problem. And it was surprise for me, that state var in View didn't change in ForEach loop (you'll see it at the screenshot). Ok, I rewrite your code, now circles going down:
// MARK: models
class GameCircles: ObservableObject {
#Published var circles: [CircleModel] = []
func addNewCircle() {
circles.append(CircleModel(x: CGFloat.random(in: -200...200), y: -200))
}
}
struct CircleModel: Identifiable, Equatable {
let id = UUID()
var x: CGFloat
var y: CGFloat
mutating func pushDown() {
self.y += 5
}
}
// MARK: views
struct GameView: View {
#State var gameSeconds = 0
#ObservedObject var game = GameCircles()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Color.primary
.frame(width: 450, height: 800)
ForEach(self.game.circles) { circle in
CircleView(y: circle.y)
.offset(x: circle.x, y: circle.y)
.onReceive(self.timer) { _ in
let circleIndex = self.game.circles.firstIndex(of: circle)!
self.game.circles[circleIndex].pushDown()
}
}
.onReceive(self.timer) { _ in
self.game.addNewCircle()
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct CircleView: View {
#State var y: CGFloat
var body: some View {
ZStack {
Circle()
.frame(width: 40, height: 40)
.foregroundColor(.red)
.animation(.default)
Text("\(Int(y))")
.foregroundColor(.black)
}
}
}
struct GameView_Previews: PreviewProvider {
static var previews: some View {
GameView()
}
}
as you can see #State var y doesn't change and I wonder why? Nevertheless, I hope it could help you.
P.S. I rewrote the code for a few times, so that is not the only solution and you may use tick() func as in the question and the code will be more clear
class GameCircles: ObservableObject {
#Published var circles: [CircleModel] = []
func tick() {
for index in circles.indices {
circles[index].pushDown()
}
circles.append(CircleModel(x: CGFloat.random(in: -200...200), y: -200))
}
}
struct GameView: View {
#State var gameSeconds = 0
#ObservedObject var game = GameCircles()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Color.primary
.frame(width: 450, height: 800)
ForEach(self.game.circles) { circle in
CircleView(y: circle.y)
.offset(x: circle.x, y: circle.y)
}
.onReceive(self.timer) { _ in
self.game.tick()
}
}
.edgesIgnoringSafeArea(.all)
}
}

SwiftUI NSManagedObject changes do not update the view

In my view model, if I update an NSManagedObject property, then the view won't update anymore. I have attached the code and the view model.
I added a comment in front of the line that breaks the view update.
class StudySessionCounterViewModel: ObservableObject {
fileprivate var studySession: StudySession
init(_ studySession: StudySession) {
self.studySession = studySession
}
#Published var elapsedTime = 14 * 60
#Published var circleProgress: Double = 0
var timer: Timer?
var formattedTime: String {
get {
let timeToFormat = (Int(studySession.studyTime) * 60) - elapsedTime
let minutes = timeToFormat / 60 % 60
let seconds = timeToFormat % 60
return String(format:"%02i:%02i", minutes, seconds)
}
}
func startTimer() {
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.timerTicked), userInfo: nil, repeats: true)
studySession.isActive = true //Adding this stops my view from updating
}
#objc func timerTicked() {
elapsedTime += 1
circleProgress = (Double(elapsedTime) / Double(studySession.studyTime * 60))
}
func stop() {
timer?.invalidate()
}
}
This is the view that uses that view model. When adding that line, the text that represents the formatted time no longer changes and the progress circle's progress remain the same.
If I remove the line, everything updates and work as expected.
struct StudySessionCounterView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var studySessionCounterVM: StudySessionCounterViewModel
var studySession: StudySession
init(_ studySession: StudySession) {
studySessionCounterVM = StudySessionCounterViewModel(studySession)
self.studySession = studySession
}
#State var showAlert = false
#State var isCounting = false
var body: some View {
VStack {
ZStack {
Text(studySessionCounterVM.formattedTime)
.font(.largeTitle)
ProgressRingView(size: .large, progress: $studySessionCounterVM.circleProgress)
}
Spacer()
if isCounting {
Button(action: {
self.studySessionCounterVM.stop()
self.isCounting = false
}) {
Image(systemName: "stop.circle")
.resizable()
.frame(width: 64, height: 64, alignment: .center)
.foregroundColor(.orange)
}
} else {
Button(action: {
self.studySessionCounterVM.startTimer()
self.isCounting = true
}) {
Image(systemName: "play.circle")
.resizable()
.frame(width: 64, height: 64, alignment: .center)
.foregroundColor(.orange)
}
}
}.padding()
.navigationBarTitle("Matematica", displayMode: .inline)
}
}
UPDATE: Found out that each time the NSManagedObject changes a property, the view model gets reinitialised. Still no solution, unfortunately