I have a fairly simple piece of code that I am trying to write containing a displayed array and a draggable object inside that array. The problem though is that it compiles and displays the array, but does not update the view when I tap and drag the object, despite the terminal showing that the tOffset variable is getting constantly updated.
I am 90% sure I am just not getting the property wrapper right, maybe #ObservedObject doesn't work for arrays? And feedback is appreciated
.
Here is my class declaration:
class Token: Identifiable, ObservableObject{
var id: UUID
var name: String
var scale: CGFloat
#Published var sOffset: CGSize
#Published var cOffset: CGSize
#Published var tOffset: CGSize
init(n: String) {
id = UUID()
name = n
scale = 2.0
sOffset = .zero
cOffset = .zero
tOffset = .zero
updateTOffset()
}
public func updateTOffset() {
tOffset.height = sOffset.height + cOffset.height
tOffset.width = sOffset.width + cOffset.width
print("Updated T Offset , x: \(tOffset.width), y:\(tOffset.height) ")
}
}
class varToken: Token {
var exp: expToken?
init(name: String, exp: expToken?) {
super.init(n: name)
self.exp = exp
}
}
class expToken: Token {
#Published var bOffset: CGSize = .zero
init(name: String) {
super.init(n: name)
sOffset.height = -10
scale = 1.0
updateTOffset()
}
}
class tokenArray: Identifiable, ObservableObject {
var id: UUID = UUID()
#Published var tokens: [varToken] = [varToken(name: "1 +", exp: nil), varToken(name: "x", exp: expToken(name: "2"))]
}
I created it intending for it to conform to Observable Object. The view is very simple, with all elements in one view struct:
struct ViewDemo2: View {
#ObservedObject var t: tokenArray = tokenArray()
var body: some View {
HStack(alignment: .bottom) {
ForEach(t.tokens) { token in
Text(token.name)
.scaleEffect(token.scale, anchor: .center)
.offset(token.tOffset)
.padding(.leading, 10)
if token.exp != nil {
Text(token.exp!.name)
.scaleEffect(token.exp!.scale)
.offset(token.exp!.tOffset)
.gesture(
DragGesture()
.onChanged() { value in
withAnimation(.spring()) {
token.exp!.cOffset = value.translation
token.exp!.updateTOffset()
}
}
.onEnded() { value in
withAnimation(.spring()) {
token.exp!.cOffset = .zero
token.exp!.updateTOffset()
}
}
)
}
}
}
}
}
Related
I'm loading data into a struct from JSON. With this data, a new structure (itemStructures) is created and filled for use in a SwiftUI View. In this View I have a #State var which I manually initialise to have enough space to hold all items. This state var holds all parameters which are nested within the items, hence the nested array.
As long as this #State var has enough empty spaces everything works fine. But my question is, how do I modify this #State programmatically for when the number of items increases er decreases with the loading of a new JSON? I could make it really large but I'd rather have it the exact size after each load.
//Structs used in this example
struct MainViewState {
var itemStructures: [ItemStructure]
}
struct ItemStructure: Identifiable, Hashable {
var id: String {name}
var name: String
var parameters: [Parameter]
}
struct Parameter: Identifiable, Hashable {
var id: String {name}
var name: String
var value: Double
var range: [Double]
}
struct ContentView: View {
//In this model json is loaded, this seemed out of scope for this question to include this
#ObservedObject var viewModel: MainViewModel
//This is the #State var which should be dynamically allocated according to the content size of "itemStructures"
//For now 3 items with 10 parameters each are enough
#State var parametersPerItem: [[Float]] = [
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0]
]
init(viewModel: MainViewModel) {
self.viewModel = viewModel
}
var body: some View {
let itemStructures = viewModel.mainState.itemStructures
ForEach( Array(itemStructures.enumerated()), id: \.element ) { index, item in
Text(item.name)
ForEach( Array(item.parameters.enumerated()), id: \.element ) { i, parameter in
Text(parameter.name)
SliderView(
label: parameter.name,
value: Binding(
get: { self.parametersPerItem[index][i] },
set: { (newVal) in
self.parametersPerItem[index][i] = newVal
//Function to send slider values and ranges to real time processing
//doStuffWithRangesAndValues()
}
),
range: parameter.range,
showsLabel: false
).onAppear {
//Set initial value slider
parametersPerItem[index][i] = Float(parameter.value)
}
}
}
}
}
struct SliderView: View {
var label: String
#Binding var value: Float
var range: [Double]
var showsLabel: Bool
init(label: String, value: Binding<Float>, range: [Double], showsLabel: Bool = true) {
self.label = label
_value = value
self.range = range
self.showsLabel = showsLabel
}
var body: some View {
GeometryReader { geometry in
ZStack{
if showsLabel { Text(label) }
HStack {
Slider(value: $value)
.foregroundColor(.accentColor)
.frame(width: geometry.size.width * 0.8)
//In the real app range calculations are done here
let valueInRange = value
Text("\(valueInRange, specifier: range[1] >= 1000 ? "%.0f" : "%.2f")")
.foregroundColor(.white)
.font(.subheadline)
.frame(width: geometry.size.width * 0.2)
}
}
}
.frame(height: 40.0)
}
}
If you are looking for a solution where you want to initialise the array after the json has been loaded you could add a computed property in an extension to the main/root json model and use it to give the #State property an initial value.
extension MainViewState {
var parametersPerItem: [[Float]] {
var array: [[Float]] = []
if let max = itemStructures.map(\.parameters.count).max(by: { $0 < $1 }) {
for _ in itemStructures {
array.append(Array(repeating: 0.0, count: max))
}
}
return array
}
}
When using Xcode 13.2.1 and SwiftUI to implement a simple slideshow, I am hitting a compile-time error in which Xcode takes about 5 minutes on my M1 to decide it cannot parse my code, and eventually gives me the error:
The compiler is unable to type-check this expression in reasonable
time; try breaking up the expression into distinct sub-expressions
I narrowed it down to the NavigationLink line near the bottom. If I comment that out, it compiles quickly with only a warning.
The following is my Minimal, Reproducible Example:
import SwiftUI
import Foundation
enum MarkerType: Double {
case unlabeled = -99
case end = -4
case start = -3
case stop = -2
case blank = -1
case image = 1
}
class LabeledImage {
let image: Image
let marker: Double
var appeared = false
init(image: Image, marker: Double) {
self.image = image
self.marker = marker
}
}
struct SlideShow {
private let maxImages: Int = 10000
var images = [LabeledImage]()
var labels = [String]()
var totalImages: Int { return self.images.count }
private var fromFolder: URL
init(fromURL: URL = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/DefaultImages")) {
self.fromFolder = fromURL
}
}
class AppState: ObservableObject {
static var docDir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
#Published var isMainMenuActive = false
#Published var loadFolder: URL = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/DefaultImages")
#Published var intervalSeconds: Double = 0.6
var saveFolder = URL(fileURLWithPath: "BCILab", relativeTo: docDir)
var labels = [String]()
var totalImages: Int = 0
var saveIndex: Int = 0
}
struct minsample: View {
#StateObject private var appState = AppState()
#State private var slideshow = SlideShow()
#State private var selection: Int = 0
private func insertAppears(_ marker: Double) {
let nmarker = marker + 100.0
}
var body: some View {
NavigationView {
ForEach(0..<slideshow.images.count-1, id: \.self) { i in
let thisImage = slideshow.images[i].image
.resizable()
.aspectRatio(contentMode: .fit)
.onAppear(perform: { insertAppears(slideshow.images[i].marker) })
let nextImage = slideshow.images[i+1].image
.resizable()
.aspectRatio(contentMode: .fit)
.onAppear(perform: { insertAppears(slideshow.images[i+1].marker) })
NavigationLink(destination: nextImage, tag: i, selection: self.$selection) { thisImage }
}
}
}
}
Generally, using an index-based solution in ForEach is a bad idea. It breaks SwiftUI's system for diffing the views and tends to lead to compile-time issues as well.
I'd start by making LabeledImage Identifiable:
class LabeledImage : Identifiable {
var id = UUID()
let image: Image
let marker: Double
var appeared = false
init(image: Image, marker: Double) {
self.image = image
self.marker = marker
}
}
(I'd also make it a struct -- more on that later)
Then, since you do need indexes to achieve your nextImage functionality, you can use .enumerated on the collection:
struct MinSample: View {
#StateObject private var appState = AppState()
#State private var slideshow = SlideShow()
#State private var selection: Int? = 0
private func insertAppears(_ marker: Double) {
let nmarker = marker + 100.0
}
var body: some View {
NavigationView {
ForEach(Array(slideshow.images.enumerated()), id: \.1.id) { (index,imageModel) in
if index < slideshow.images.count - 1 {
let thisImage = imageModel.image
.resizable()
.aspectRatio(contentMode: .fit)
.onAppear(perform: { insertAppears(imageModel.marker) })
let nextImage = slideshow.images[index + 1].image
.resizable()
.aspectRatio(contentMode: .fit)
.onAppear(perform: { insertAppears(slideshow.images[index+1].marker) })
NavigationLink(destination: nextImage, tag: index, selection: self.$selection) { thisImage }
} else {
EmptyView()
}
}
}
}
}
The above compiles quickly on my M1 with no issues.
Now, not directly-related to your issue, but there are some other things I would change:
Make your models structs, which SwiftUI generally deals with much better when doing state comparisons
Don't store references to SwiftUI Images -- instead, store a reference to a path or some other way to recreate that image. That will make the transition for LabeledImage to a struct easier anyway. So, your model might look like this:
struct LabeledImage : Identifiable {
var id = UUID()
let imageName: String
let marker: Double
var appeared = false
}
Consider whether or not you need the tag and selection parameters in your NavigationLink -- perhaps it's just not clear in the minimal example why they're used.
I tried to encapsulate the gesture in a class and then call it in another view.
My project can run well, and it can be built well. but Xcode12 give me a pink error. This is my code
class GestureClass: ObservableObject {
private var minZoom:CGFloat=1
private var maxZoom:CGFloat=4.00
#GestureState private var magnificationLevel:CGFloat=1
#Published public var zoomLevel:CGFloat=1
#Published public var current:CGFloat=1
var magnify: some Gesture {
MagnificationGesture()
.updating($magnificationLevel, body:{(value,state,_) in
return state=value
})
.onChanged({ value in
self.current = value
})
.onEnded({ value in
self.zoomLevel = self.setZoom(value)
self.current = 1
})
}
func setZoom(_ gesturevalue:CGFloat) -> CGFloat {
return max(min(self.zoomLevel*gesturevalue, self.maxZoom), self.minZoom)
}
func gestureresult() -> CGFloat {
return self.setZoom(self.current)
}
}
struct GestureModelView: View {
#EnvironmentObject var gesturemodel:GestureClass
var body: some View {
let myges = gesturemodel.magnify
HStack {
Rectangle()
.foregroundColor(.blue)
.frame(width:200*gesturemodel.gestureresult(), height:200)
.gesture(myges)
}
}
}
I deleted this code in the computed property, and the error disappeared. But i really don't know why。
.updating($magnificationLevel, body:{(value,state,_) in
return state=value
})
XCode Version 12.5 (12E262) - Swift 5
To simplify this example, I've created a testObj class and added a few items to an array.
Let's pretend that I want to render buttons on the screen (see preview below), once you click on the button, it should set testObj.isSelected = true which triggers the button to change the background color.
I know it's changing the value to true, however is not triggering the button to change its color.
Here's the example:
//
// TestView.swift
//
import Foundation
import SwiftUI
import Combine
struct TestView: View {
#State var arrayOfTestObj:[testObj] = [
testObj(label: "test1"),
testObj(label: "test2"),
testObj(label: "test3")
]
var body: some View {
VStack {
ForEach(arrayOfTestObj, id: \.id) { o in
HStack {
Text(o.label)
.width(200)
.padding(20)
.background(Color.red.opacity(o.isSelected ? 0.4: 0.1))
.onTapGesture {
o.isSelected.toggle()
}
}
}
}
}
}
class testObj: ObservableObject {
let didChange = PassthroughSubject<testObj, Never>()
var id:String = UUID().uuidString {didSet {didChange.send((self))}}
var label:String = "" {didSet {didChange.send((self))}}
var value:String = "" {didSet {didChange.send((self))}}
var isSelected:Bool = false {didSet {didChange.send((self))}}
init (label:String? = "") {
self.label = label!
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
If I update the ForEach as...
ForEach($arrayOfTestObj, id: \.id) { o in
... then I get this error:
Key path value type '_' cannot be converted to contextual type '_'
How can I change testObj to make it bindable?
Any help is greatly appreciated.
struct TestView: View {
#State var arrayOfTestObj:[TestObj] = [
TestObj(label: "test1"),
TestObj(label: "test2"),
TestObj(label: "test3")
]
var body: some View {
VStack {
ForEach(arrayOfTestObj, id: \.id) { o in
//Use a row view
TestRowView(object: o)
}
}
}
}
//You can observe each object by creating a RowView
struct TestRowView: View {
//And by using this wrapper you observe changes
#ObservedObject var object: TestObj
var body: some View {
HStack {
Text(object.label)
.frame(width:200)
.padding(20)
.background(Color.red.opacity(object.isSelected ? 0.4: 0.1))
.onTapGesture {
object.isSelected.toggle()
}
}
}
}
//Classes and structs should start with a capital letter
class TestObj: ObservableObject {
//You don't have to declare didChange if you need to update manually use the built in objectDidChange
let id:String = UUID().uuidString
//#Published will notify of changes
#Published var label:String = ""
#Published var value:String = ""
#Published var isSelected:Bool = false
init (label:String? = "") {
self.label = label!
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
I have a data model that I want to use its data in various views. So firstly I have problem polling that information from my data model and secondly I have problem to update the data.
Here in one example of my data model with three views.
Data Model
// A basic resocrd of each exercise in a workout
class WorkoutItem:ObservableObject, Identifiable{
var id:Int = 0
var name: String = "An Exercise"
var sets:Int = 3
var reps:Int = 10
func addSet() {
sets += 1
}
init(){
}
}
// The model for holding a workout
class WorkoutModel:ObservableObject{
#Published var workout:[WorkoutItem] = []
var lastID:Int = -1
/// Creates a newID based on the last known ID
private func newId()->Int{
lastID += 1
return lastID
}
func add(name:String, sets:Int, reps:Int){
let newExercise = WorkoutItem()
workout += [newExercise]
}
init() {
add(name: "Psuh Pp", sets: 1, reps: 8)
add(name: "Pull UPs", sets: 1, reps: 10)
}
}
View 1
struct ContentView: View {
#ObservedObject var myWorkout: WorkoutModel
var body: some View {
List(self.myWorkout.workout) { item in
VStack(alignment: .leading) {
VStack(alignment: .leading) {
Text(item.name).font(.headline)
ExerciseEntryView(exItem: item)
}
}
}
}
}
View 2
struct ExerciseEntryView: View {
var exItem: WorkoutItem
var body: some View {
VStack {
VStack(alignment: .leading){
ForEach(0 ..< exItem.sets, id: \.self){ row in
ExerciseEntryView_Row(setNumber: row)
}
}
Button(action: {
self.exItem.addSet()
}) {
Text("Add Set").foregroundColor(Color.red)
}
}
}
}
View 3
struct ExerciseEntryView_Row: View {
var setNumber: Int
var body: some View {
HStack{
Text("Set Number \(setNumber)")
}
}
}
Firstly when running the code, as you can see in the below image the title is still the default value ('An Exercise') while it should Pull up and Push up. Also when I press on add set the set gets updated in the terminal but it does not update the view.
Any idea why this is not functioning?
Thanks in advance.
First you can make your WorkoutItem a struct:
struct WorkoutItem: Identifiable {
var id: Int = 0
var name: String = "An Exercise"
var sets: Int = 3
var reps: Int = 10
mutating func addSet() {
sets += 1
}
}
Then in the WorkoutModel you weren't using setting any properties for the new exercise:
class WorkoutModel: ObservableObject {
...
func add(name: String, sets: Int, reps: Int) {
var newExercise = WorkoutItem()
newExercise.name = name // <- set properties
newExercise.sets = sets
newExercise.reps = reps
workout += [newExercise]
}
init() {
add(name: "Push Pp", sets: 1, reps: 8)
add(name: "Pull UPs", sets: 1, reps: 10)
}
}