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)
}
}
Related
As I am working on a study app, I'm tring to build a set of cards, that a user can swipe each individual card, in this case a view, in a foreach loop and when flipped through all of them, it resets the cards to normal stack. The program works but sometimes the stack of cards doesn't reset. Each individual card updates a variable in a viewModel which my conditional view modifier looks at, to reset the stack of cards using offset and when condition is satisfied, the card view updates, while using ".onChange" to look for the change in the viewModel to then update the variable back to original state.
I've printed each variable at each step of the way and every variable updates and I can only assume that the way I'm updating my view, using conditional view modifier, may not be the correct way to go about. Any suggestions will be appreciated.
Here is my code:
The view that houses the card views with the conditional view modifier
extension View {
#ViewBuilder func `resetCards`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition == true {
transform(self).offset(x: 0, y: 0)
} else {
self
}
}
}
struct StudyListView: View {
#ObservedObject var currentStudySet: HomeViewModel
#ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
#State var studyItem: StudyModel
#State var index: Int
var body: some View {
ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
StudyCardItemView(currentCard: studyCards, card: item, count: currentStudySet.allSets[index].studyItem.count)
.resetCards(studyCards.isDone) { view in
view
}
.onChange(of: studyCards.isDone, perform: { _ in
studyCards.isDone = false
})
}
}
}
StudyCardItemView
struct StudyCardItemView: View {
#StateObject var currentCard: StudyListViewModel
#State var card: StudyItemModel
#State var count: Int
#State var offset = CGSize.zero
var body: some View {
VStack{
VStack{
ZStack(alignment: .center){
Text("\(card.itemTitle)")
}
}
}
.frame(width: 350, height: 200)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.padding(5)
.rotationEffect(.degrees(Double(offset.width / 5)))
.offset(x: offset.width * 5, y: 0)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded{ _ in
if abs(offset.width) > 100 {
currentCard.cardsSortedThrough += 1
if (currentCard.cardsSortedThrough == count) {
currentCard.isDone = true
currentCard.cardsSortedThrough = 0
}
} else {
offset = .zero
}
}
)
}
}
HomeViewModel
class HomeViewModel: ObservableObject {
#Published var studySet: StudyModel = StudyModel()
#Published var allSets: [StudyModel] = [StudyModel()]
}
I initialize allSets with one StudyModel() to see it in the preview
StudyListViewModel
class StudyListViewModel: ObservableObject {
#Published var cardsSortedThrough: Int = 0
#Published var isDone: Bool = false
}
StudyModel
import SwiftUI
struct StudyModel: Hashable{
var title: String = ""
var days = ["One day", "Two days", "Three days", "Four days", "Five days", "Six days", "Seven days"]
var studyGoals = "One day"
var studyItem: [StudyItemModel] = []
}
Lastly, StudyItemModel
struct StudyItemModel: Hashable, Identifiable{
let id = UUID()
var itemTitle: String = ""
var itemDescription: String = ""
}
Once again, any help would be appreciated, thanks in advance!
I just found a fix and I put .onChange at the end for StudyCardItemView. Basically, the onChange helps the view scan for a change in currentCard.isDone variable every time it was called in the foreach loop and updates offset individuality. This made my conditional view modifier obsolete and just use the onChange to check for the condition.
I still used onChange outside the view with the foreach loop, just to set currentCard.isDone variable false because the variable will be set after all array elements are iterator through.
The updated code:
StudyCardItemView
struct StudyCardItemView: View {
#StateObject var currentCard: StudyListViewModel
#State var card: StudyItemModel
#State var count: Int
#State var offset = CGSize.zero
var body: some View {
VStack{
VStack{
ZStack(alignment: .center){
Text("\(card.itemTitle)")
}
}
}
.frame(width: 350, height: 200)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.padding(5)
.rotationEffect(.degrees(Double(offset.width / 5)))
.offset(x: offset.width * 5, y: 0)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded{ _ in
if abs(offset.width) > 100 {
currentCard.cardsSortedThrough += 1
if (currentCard.cardsSortedThrough == count) {
currentCard.isDone = true
currentCard.cardsSortedThrough = 0
}
} else {
offset = .zero
}
}
)
.onChange(of: currentCard.isDone, perform: {_ in
offset = .zero
})
}
}
StudyListView
struct StudyListView: View {
#ObservedObject var currentStudySet: HomeViewModel
#ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
#State var studyItem: StudyModel
#State var index: Int
var body: some View {
ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
StudyCardItemView(currentCard: studyCards, card: item, count:
currentStudySet.allSets[index].studyItem.count)
.onChange(of: studyCards.isDone, perform: { _ in
studyCards.isDone = false
})
}
}
}
Hope this helps anyone in the future!
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
I'm working on some code to teach myself SwiftUI, and my current project is a dice roller app.
In my view, I present some images using SFSymbols to represent the value of each die after the roll. I use two animations (.rotation3Deffect and .rotationEffect) on each image to give a visual roll of the dice.
I have found that the animations will only run if the individual die value (or in case of the code, if the Image(systemName: "\(diceValue).square.fill")) changes from one roll to the next. In other words, if I roll three dice, and they land on 1-4-6, then I roll again and they land on 1-4-7, the 1 and 4 images will perform the animation, but the image changing from 6 to 7 does not animate - it just updates to the new image. Any idea why the animation only runs on the images that did not change from the previous dice roll?
import SwiftUI
struct Dice {
var sides: Int
var values: [Int] {
var calculatedValues = [Int]()
for v in 1...sides {
calculatedValues.append(v)
}
return calculatedValues
}
func rollDice() -> Int {
let index = Int.random(in: 0..<values.count)
return values[index]
}
}
struct Roll: Hashable, Identifiable {
let id = UUID()
var diceNumber: Int = 0
var diceValue: Int = 0
}
struct DiceThrow: Hashable {
var rolls = [Roll]()
var totalScore: Int {
var score = 0
for roll in rolls {
score += roll.diceValue
}
return score
}
}
class ThrowStore: ObservableObject, Hashable {
#Published var diceThrows = [DiceThrow]()
static func ==(lhs: ThrowStore, rhs: ThrowStore) -> Bool {
return lhs.diceThrows == rhs.diceThrows
}
func hash(into hasher: inout Hasher) {
hasher.combine(diceThrows)
}
}
class Settings: ObservableObject {
#Published var dicePerRoll: Int = 1
#Published var sidesPerDice: Int = 4
init(dicePerRoll: Int, sidesPerDice: Int) {
self.dicePerRoll = dicePerRoll
self.sidesPerDice = sidesPerDice
}
}
struct ContentView: View {
var throwStore = ThrowStore()
let settings = Settings(dicePerRoll: 3, sidesPerDice: 6)
#State private var roll = Roll()
#State private var diceThrow = DiceThrow()
#State private var isRotated = false
#State private var rotationAngle: Double = 0
var animation: Animation {
Animation.easeOut(duration: 3)
}
var body: some View {
VStack {
if self.throwStore.diceThrows.count != 0 {
HStack {
Text("For dice roll #\(self.throwStore.diceThrows.count)...")
.fontWeight(.bold)
.font(.largeTitle)
.padding(.leading)
Spacer()
}
}
List {
HStack(alignment: .center) {
Spacer()
ForEach(diceThrow.rolls, id: \.self) { printedRoll in
Text("\(printedRoll.diceValue)")
.font(.title)
.foregroundColor(.white)
.frame(width: 50, height: 50)
.background(RoundedRectangle(cornerRadius: 4))
.rotation3DEffect(Angle.degrees(isRotated ? 3600 : 0), axis: (x: 1, y: 0, z: 0))
.rotationEffect(Angle.degrees(isRotated ? 3600 : 0))
.animation(animation)
}
Spacer()
}
if self.throwStore.diceThrows.count != 0 {
HStack {
Text("Total for this roll:")
Spacer()
Text("\(self.diceThrow.totalScore)")
.padding(.leading)
.padding(.trailing)
.background(Capsule().stroke())
}
}
}
Spacer()
Button("Roll the dice") {
self.diceThrow.rolls = [Roll]()
self.isRotated.toggle()
for diceNumber in 1...settings.dicePerRoll {
let currentDice = Dice(sides: settings.sidesPerDice)
roll.diceNumber = diceNumber
roll.diceValue = currentDice.rollDice()
self.diceThrow.rolls.append(roll)
}
self.throwStore.diceThrows.append(self.diceThrow)
}
.frame(width: 200)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.clipShape(Capsule())
.padding(.bottom)
}
}
func showDiceRoll(_ sides: Int) {
}
}
In your scenario the data identifiers in ForEach should remain the same, otherwise corresponding views are replaced and animation does not work.
The fix for this is simple - use diceNumber as ID:
ForEach(diceThrow.rolls, id: \.diceNumber) { printedRoll in
Text("\(printedRoll.diceValue)")
Tested with Xcode 12.1 / iOS 14.1
Hey does someone know how can I create rectangle progress bar in swiftUI?
Something like this?
https://i.stack.imgur.com/CMwB3.gif
I have tried this:
struct ProgressBar: View
{
#State var degress = 0.0
#Binding var shouldLoad: Bool
var body: some View
{
RoundedRectangle(cornerRadius: cornerRadiusValue)
.trim(from: 0.0, to: CGFloat(degress))
.stroke(Color.Scheme.main, lineWidth: 2.0)
.frame(width: 300, height: 40, alignment: .center)
.onAppear(perform: shouldLoad == true ? {self.start()} : {})
}
func start()
{
Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true)
{
timer in
withAnimation
{
self.degress += 0.3
}
}
}
}
Here is simple demo of possible approach for [0..1] range progress indicator.
Updated: re-tested with Xcode 13.3 / iOS 15.4
struct ProgressBar: View {
#Binding var progress: CGFloat // [0..1]
var body: some View {
RoundedRectangle(cornerRadius: 10)
.trim(from: 0.0, to: CGFloat(progress))
.stroke(Color.red, lineWidth: 2.0)
.animation(.linear, value: progress)
}
}
struct DemoAnimatingProgress: View {
#State private var progress = CGFloat.zero
var body: some View {
Button("Demo") {
if self.progress == .zero {
self.simulateLoading()
} else {
self.progress = 0
}
}
.padding()
.background(ProgressBar(progress: $progress))
}
func simulateLoading() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.progress += 0.1
if self.progress < 1.0 {
self.simulateLoading()
}
}
}
}
Available for XCode 12.
import SwiftUI
//MARK: - ProgressBar
struct ContentView: View {
#State private var downloaded = 0.0
var body: some View {
ProgressView("Downloaded...", value: downloaded, total: 100)
}
}
//MARK: - Circular ProgressBar
struct ContentView: View {
#State private var downloaded = 0.0
var body: some View {
ProgressView("Downloaded...", value: downloaded, total: 100)
.progressViewStyle(CircularProgressViewStyle())
}
}
In my SwiftUI app, I need to get data from ObservedObject each time the value change. I understood that we could do that with .onReceive? I don't understand well the documentation of Apple about it. I don't know how I can do this.
My code:
import SwiftUI
import CoreLocation
struct Compass: View {
#StateObject var location = LocationManager()
#State private var angle: CGFloat = 0
var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.modifier(RotationEffect(angle: -CGFloat(self.angle.degreesToRadians)))
.onReceive(location, perform: {
withAnimation(.easeInOut(duration: 1.0)) {
self.angle = self.location.heading
}
})
Text(String(self.location.heading.degreesToRadians))
.font(.system(size: 20))
.fontWeight(.light)
.padding(.top, 15)
}
}
}
struct RotationEffect: GeometryEffect {
var angle: CGFloat
var animatableData: CGFloat {
get { angle }
set { angle = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(
CGAffineTransform(translationX: -150, y: -150)
.concatenating(CGAffineTransform(rotationAngle: angle))
.concatenating(CGAffineTransform(translationX: 150, y: 150))
)
}
}
In my LocationManager class, I have a heading Published variable, this is the variable I want check.
I need to get data each time the value of heading change to create an animation when my arrow move. For some raisons I need to use CGAffineTransform.
First in your view you need to request the HeadingProvider to start updating heading. You need to listen to objectWillChange notification, the closure has one argument which is the new value that is being set on ObservableObject.
I have changed your Compass a bit:
struct Compass: View {
#StateObject var headingProvider = HeadingProvider()
#State private var angle: CGFloat = 0
var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.modifier(RotationEffect(angle: angle))
.onReceive(self.headingProvider.objectWillChange) { newHeading in
withAnimation(.easeInOut(duration: 1.0)) {
self.angle = newHeading
}
}
Text(String("\(angle)"))
.font(.system(size: 20))
.fontWeight(.light)
.padding(.top, 15)
} .onAppear(perform: {
self.headingProvider.updateHeading()
})
}
}
I have written an example HeadingProvider:
public class HeadingProvider: NSObject, ObservableObject {
public let objectWillChange = PassthroughSubject<CGFloat,Never>()
public private(set) var heading: CGFloat = 0 {
willSet {
objectWillChange.send(newValue)
}
}
private let locationManager: CLLocationManager
public override init(){
self.locationManager = CLLocationManager()
super.init()
self.locationManager.delegate = self
}
public func updateHeading() {
locationManager.startUpdatingHeading()
}
}
extension HeadingProvider: CLLocationManagerDelegate {
public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
DispatchQueue.main.async {
self.heading = CGFloat(newHeading.trueHeading)
}
}
}
Remember you need to handle asking for permission to read user's location and you need to call stopUpdatingHeading() at some point.
You can consider using #Published in your ObservableObject. Then your onreceive can get a call by using location.$heading.
For observable object it can be
class LocationManager: ObservableObject {
#Published var heading:Angle = Angle(degrees: 20)
}
For the receive you can use
struct Compass: View {
#ObservedObject var location: LocationManager = LocationManager()
#State private var angle: Angle = Angle(degrees:0)
var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.modifier(RotationEffect(angle: angle))
.onReceive(location.$heading, perform: { heading in
withAnimation(.easeInOut(duration: 1.0)) {
self.angle = heading
}
})
}
}
}
The above is useful if you want to perform additional functions on object changes. In many cases you can directly use the location.heading as your state changer. And then give it an animation below it. So
.modifier(RotationEffect(angle: location.heading))
.animation(.easeInOut)
iOS 14+
Starting from iOS 14 we can now use the onChange modifier that performs an action every time a value is changed:
Here is an example:
struct Compass: View {
#StateObject var location = LocationManager()
#State private var angle: CGFloat = 0
var body: some View {
VStack {
// ...
}
.onChange(of: location.heading) { heading in
withAnimation(.easeInOut(duration: 1.0)) {
self.angle = heading
}
}
}
}
You can do as #LuLuGaGa suggests but it's a bit of a kludge. objectWillChange is defined as an ObservableObjectPublisher and while defining it as a PassthroughSubject<CGFloat,Never> is going to work today there is no guarantee that it will work in the future.
An object is not restricted to having a single publisher so you can define a second or a third for other purposes than SwiftUI. e.g.:
class Observable<T>: ObservableObject, Identifiable {
let id = UUID()
let publisher = PassthroughSubject<T, Never>()
var value: T {
willSet { objectWillChange.send() }
didSet { publisher.send(value) }
}
init(_ initValue: T) { self.value = initValue }
}
Subclassing ObservableObject will correctly define objectWillChange for you so you don't have to do it yourself.