I am making a Slider based on a view model, but I am facing this error message Initializer 'init(value:in:step:label:minimumValueLabel:maximumValueLabel:onEditingChanged:)' requires that 'Int.Stride' (aka 'Int') conform to 'BinaryFloatingPoint'
It is strange because converting the integer from view model into Double doesn't quite do the trick.
I found very similar question and read the SO answer (How can I make Int conform to BinaryFloatingPoint or Double/CGFloat conform to BinaryInteger?), but it doesn't seem like I can implementation the solution for my case, probably because I am using ObservedObject for the view model.
If I remove $ in front of setInformationVM.elapsedRestTime, I would see another error message saying Cannot convert value of type 'Int' to expected argument type 'Binding<Int>'
They said "Binding are generally used when there is a need for 2-way communication" - would that mean the Slider needs a way to communicate/update back to the View Model? Why is it that the Slider was accepting #State private var xx: Double for the value in general , but not a simple integer from my view model?
import Foundation
import SwiftUI
import Combine
struct SetRestDetailView: View {
#EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
#State var showingLog = false
var body: some View {
GeometryReader { geometry in
ZStack() {
(view content removed for readability)
}
.sheet(isPresented: $showingLog) {
let setInformatationVM = self.watchDayProgramVM.exerciseVMList[0].sets[2]
setLoggingView(setInformationVM: setInformatationVM, restfullness: 3, stepValue: 10)
}
}
}
setLoggingView
struct setLoggingView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var setInformationVM: SetInformationTestClass
#State var restfullness: Int
var stepValue: Int
var body: some View {
GeometryReader { geometry in
let rect = geometry.frame(in: .global)
ScrollView {
VStack(spacing: 5) {
Text("Rested \(Int(setInformationVM.elapsedRestTime)) sec")
Slider(value: $setInformationVM.elapsedRestTime,
in: 0...setInformationVM.totalRestTime,
step: Int.Stride(stepValue),
label: {
Text("Slider")
}, minimumValueLabel: {
Text("-\(stepValue)")
}, maximumValueLabel: {
Text("+\(stepValue)")
})
.tint(Color.white)
.padding(.bottom)
Divider()
Spacer()
Text("Restfullness")
.frame(minWidth: 0, maxWidth: .infinity)
restfullnessStepper(rect: rect, maxRestFullness: 5, minRestFullness: 1, restfullnessIndex: restfullness)
Button(action: {
print("Update Button Pressed")
//TODO
//perform further actions to update restfullness metric and elapsed rest time in the viewmodels before dismissing the view, and also update the iOS app by synching the view model.
dismiss()
}) {
HStack {
Text("Update")
.fontWeight(.medium)
}
}
.cornerRadius(40)
}
.border(Color.yellow)
}
}
}
SetInformationTestClass view model
class SetInformationTestClass: ObservableObject {
init(totalRestTime: Int, elapsedRestTime: Int, remainingRestTime: Int, isTimerRunning: Bool) {
self.totalRestTime = totalRestTime
self.elapsedRestTime = elapsedRestTime
self.remainingRestTime = remainingRestTime
}
#Published var totalRestTime: Int
#Published var elapsedRestTime: Int
#Published var remainingRestTime: Int
You can create a custom binding variable like :
let elapsedTime = Binding(
get: { Double(self.setInformationVM.elapsedRestTime) },
set: { self.setInformationVM.elapsedRestTime = Int($0) } // Or other custom logic
)
// then you reference it in the slider like:
Slider(elapsedTime, ...)
Related
I'm learning about SwiftUI generics and I have having some issues when defining a #ObservedObject variable; itself an extension of a struct that has generic arguments. I'm getting the following error when setting:
#ObservedObject var vm = GenericView.GenericViewModel
Error:
Reference to generic type '_' requires arguments in <...>
Xcode is asking me to explicitly define my generics, but everything that I have tried so far has failed. What exactly is it that Xcode wants me to pass? Here is some sample code:
GenericView
import SwiftUI
struct GenericView<T: Shape, U: Shape, V: View>: View {
#StateObject var vm = GenericView.GenericViewModel()
let shape1: T
let shape2: U
let color1: Color
let color2: Color
let color3: Color
let content: () -> V
var body: some View {
ZStack {
HStack {
shape1
.foregroundColor(color1)
shape2
.foregroundColor(color2)
}
.overlay(
VStack {
content()
Text(vm.someData)
}
.foregroundColor(color3)
)
}
.frame(width: 200,
height: 100)
}
}
GenericViewModel
import Foundation
extension GenericView {
class GenericViewModel: ObservableObject {
#Published var someData = ""
init() { self.someData = someData }
}
}
ContentView
import SwiftUI
struct ContentView: View {
#ObservedObject var vm = GenericView.GenericViewModel
var body: some View {
VStack {
GenericView(vm: vm,
shape1: Circle(),
shape2: Rectangle(),
color1: .black,
color2: .gray,
color3: .white) {
Text("Hello, world.")
}
Button {
vm.someData = "Hello back."
} label: {
Rectangle()
.overlay(
Text("Respond")
.foregroundColor(.black)
.bold()
)
.frame(width: 100,
height: 50)
}
.padding()
}
}
}
The modifications to make it correct :
Definition of GenericView
struct GenericView<T: Shape, U: Shape, V: View>: View {
// This observed object : it does not belong to Generci view
#ObservedObject var vm: GenericViewModel
...
Declaration of model :
struct ContentView: View {
// The model belongs to content view
// the model var must declare the type of the generic view
#StateObject var vm = GenericView<Circle, Rectangle, Text>.GenericViewModel()
...
Given the setup I've outlined below, I'm trying to determine why ChildView's .onChange(of: _) is not receiving updates.
import SwiftUI
struct SomeItem: Equatable {
var doubleValue: Double
}
struct ParentView: View {
#State
private var someItem = SomeItem(doubleValue: 45)
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { someItem.doubleValue += 10.0 }
.overlay { ChildView(someItem: $someItem) }
}
}
struct ChildView: View {
#StateObject
var viewModel: ViewModel
init(someItem: Binding<SomeItem>) {
_viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.someItem) { _ in
print("Change Detected", viewModel.someItem.doubleValue)
}
}
}
#MainActor
final class ViewModel: ObservableObject {
#Binding
var someItem: SomeItem
public init(someItem: Binding<SomeItem>) {
self._someItem = someItem
}
public func changeItem() {
self.someItem = SomeItem(doubleValue: .zero)
}
}
Interestingly, if I make the following changes in ChildView, I get the behavior I want.
Change #StateObject to #ObservedObject
Change _viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem)) to viewModel = ViewModel(someItem: someItem)
From what I understand, it is improper for ChildView's viewModel to be #ObservedObject because ChildView owns viewModel but #ObservedObject gives me the behavior I need whereas #StateObject does not.
Here are the differences I'm paying attention to:
When using #ObservedObject, I can tap the black area and see the changes applied to both the white text and red rectangle. I can also tap the red rectangle and see the changes observed in ParentView through the white text.
When using #StateObject, I can tap the black area and see the changes applied to both the white text and red rectangle. The problem lies in that I can tap the red rectangle here and see the changes reflected in ParentView but ChildView doesn't recognize the change (rotation does not change and "Change Detected" is not printed).
Is #ObservedObject actually correct since ViewModel contains a #Binding to a #State created in ParentView?
Normally, I would not write such a convoluted solution to a problem, but it sounds like from your comments on another answer there are certain architectural issues that you are required to conform to.
The general issue with your initial approach is that onChange is only going to run when the view has a render triggered. Generally, that happens because some a passed-in property has changed, #State has changed, or a publisher on an ObservableObject has changed. In this case, none of those are true -- you have a Binding on your ObservableObject, but nothing that triggers the view to re-render. If Bindings provided a publisher, it would be easy to hook into that value, but since they do not, it seems like the logical approach is to store the state in the parent view in a way in which we can watch a #Published value.
Again, this is not necessarily the route I would take, but hopefully it fits your requirements:
struct SomeItem: Equatable {
var doubleValue: Double
}
class Store : ObservableObject {
#Published var someItem = SomeItem(doubleValue: 45)
}
struct ParentView: View {
#StateObject private var store = Store()
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(store.someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { store.someItem.doubleValue += 10.0 }
.overlay { ChildView(store: store) }
}
}
struct ChildView: View {
#StateObject private var viewModel: ViewModel
init(store: Store) {
_viewModel = StateObject(wrappedValue: ViewModel(store: store))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.store.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.store.someItem.doubleValue) { _ in
print("Change Detected", viewModel.store.someItem.doubleValue)
}
}
}
#MainActor
final class ViewModel: ObservableObject {
var store: Store
var cancellable : AnyCancellable?
public init(store: Store) {
self.store = store
cancellable = store.$someItem.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
public func changeItem() {
store.someItem = SomeItem(doubleValue: .zero)
}
}
Actually we don't use view model objects at all in SwiftUI, see [Data Essentials in SwiftUI WWDC 2020]. As shown in the video at 4:33 create a custom struct to hold the item, e.g. ChildViewConfig and init it in an #State in the parent. Set the childViewConfig.item in a handler or add any mutating custom funcs. Pass the binding $childViewConfig or $childViewConfig.item to the to the child View if you need write access. It's all very simple if you stick to structs and value semantics.
I am trying to create a list using ForEach and NavigationLink of an array of data.
I believe my code (see the end of the post) is correct but my build fails due to
"Missing argument for parameter 'index' in call" and takes me to SceneDelegate.swift a place I haven't had to venture before.
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
I can get the code to run if I amend to;
let contentView = ContentView(habits: HabitsList(), index: 1)
but then all my links hold the same data, which makes sense since I am naming the index position.
I have tried, index: self.index (which is what I am using in my NavigationLink) and get a different error message - Cannot convert value of type '(Any) -> Int' to expected argument type 'Int'
Below are snippets of my code for reference;
struct HabitItem: Identifiable, Codable {
let id = UUID()
let name: String
let description: String
let amount: Int
}
class HabitsList: ObservableObject {
#Published var items = [HabitItem]()
}
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var index: Int
var body: some View {
NavigationView {
List {
ForEach(habits.items) { item in
NavigationLink(destination: HabitDetail(habits: self.habits, index: self.index)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}
}
}
}
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var habits: HabitsList
var index: Int
var body: some View {
NavigationView {
Form {
Text(self.habits.items[index].name)
}
}
}
}
You probably don't need to pass the whole ObservedObject to the HabitDetail.
Passing just a HabitItem should be enough:
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
let item: HabitItem
var body: some View {
// remove `NavigationView` form the detail view
Form {
Text(item.name)
}
}
}
Then you can modify your ContentView:
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var body: some View {
NavigationView {
List {
// for every item in habits create a `linkView`
ForEach(habits.items, id:\.id) { item in
self.linkView(item: item)
}
}
}
}
// extract to another function for clarity
func linkView(item: HabitItem) -> some View {
// pass just a `HabitItem` to the `HabitDetail`
NavigationLink(destination: HabitDetail(item: item)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}
Creating a simple card game (Set) and I have a function in the model that deals X cards onto the deck. Currently, when I click the deal card button they all show up at once so I added the timer so that they would appear one after another. This gives the error "Escaping closure captures mutating 'self' parameter" Any ideas on what I can fix?
mutating func deal(_ numberOfCards: Int) {
for i in 0..<numberOfCards {
Timer.scheduledTimer(withTimeInterval: 0.3 * Double(i), repeats: false) { _ in
if deck.count > 0 {
dealtCards.append(deck.removeFirst())
}
}
}
}
A timer is not even required. You can use transitions in combination with an animation to get the desired effect. Here the transition is delayed based on the index of the card:
class CardModel: ObservableObject {
#Published var cards: [Int] = []
func deal(_ numberOfCards: Int) {
cards += (cards.count ..< (cards.count + numberOfCards)).map { $0 }
}
func clear() {
cards = []
}
}
struct ContentView: View {
#ObservedObject var cardModel = CardModel()
var body: some View {
GeometryReader { geometry in
VStack {
HStack {
ForEach(0 ..< self.cardModel.cards.count, id: \.self) { index in
CardView(cardNumber: self.cardModel.cards[index])
.transition(.offset(x: geometry.size.width))
.animation(Animation.easeOut.delay(Double(index) * 0.1))
}
}
Button(action: { self.cardModel.deal(2) }) {
Text("Deal")
}
Button(action: { self.cardModel.clear() }) {
Text("Clear")
}
}
}
}
}
struct CardView: View {
let cardNumber: Int
var body: some View {
Rectangle()
.foregroundColor(.green)
.frame(width: 9, height: 16)
}
}
Or a bit simpler (without the CardModel):
struct ContentView: View {
#State var cards: [Int] = []
func deal(_ numberOfCards: Int) {
cards += (cards.count ..< (cards.count + numberOfCards)).map { $0 }
}
func clear() {
cards = []
}
var body: some View {
GeometryReader { geometry in
VStack {
HStack {
ForEach(0 ..< self.cards.count, id: \.self) { index in
CardView(cardNumber: self.cards[index])
.transition(.offset(x: geometry.size.width))
.animation(Animation.easeOut.delay(Double(index) * 0.1))
}
}
Button(action: { self.deal(2) }) {
Text("Deal")
}
Button(action: { self.clear() }) {
Text("Clear")
}
}
}
}
}
struct CardView: View {
let cardNumber: Int
var body: some View {
Rectangle()
.foregroundColor(.green)
.frame(width: 9, height: 16)
}
}
Note this approach works fine if you're centering the cards, because the previous cards will need to shift too. If you left-align the cards however (using a spacer) the animation delay will be present for the cards that do not need to shift (the animation starts with an awkward delay). If you need to account for this case, you'll need to make the newly inserted index part of the model.
(This may be duplicating what Jack Goossen has written; go look there first, and if it's not clear, this may give some more explanation.)
The core problem here is that you appear to be treating a struct as a reference type. A struct is a value. That means that each holder of it has its own copy. If you pass a value to a Timer, then the Timer is mutating its own copy of that value, which can't be self.
In SwiftUI, models are typically reference types (classes). They represent an identifiable "thing" that can be observed and changes over time. Changing this type to a class would likely address your problem. (See Jack Goossen's answer, which uses a class to hold the cards.)
This is backwards of the direction that Swift had been moving in with UIKit, where views were reference types and the model was encouraged to be made of value types. In SwiftUI, views are structs, and the model is usually made of classes.
(Using Combine with SwiftUI, it's possible to make both view and model into value types, but that's possibly beyond what you were trying to do here, and is a bit more complex if you haven't studied Combine or reactive programming already.)
In the following code I have a struct that has different values.
struct Workout: Codable {
let exercise: [Exercise]
enum CodingKeys: String, CodingKey {
case exercise = "exercise"
}
}
struct Exercise: Codable {
let exercise, set: Int
}
class WorkoutViewModel: ObservableObject {
#Published var workout: Workout
init(_ workout: Workout) {
self.workout = workout
}
}
In my main view I call initialise the value as this:
struct CreateWorkoutView: View {
#ObservedObject var myExercises = WorkoutViewModel(Workout(exercise: [Exercise(exercise: 1, set: 1)]))
var body: some View {
NavigationView {
NavigationLink(
destination: View2(exerciseList: self.$myExercises.workout.exercise)
){
Text("Add Exercises")
}
}
struct View2: View {
#ObservedObject var excersices = getExercisesData()
#Binding var exerciseList: [Exercise]
var body: some View {
VStack {
List(excersices.datas){i in
HStack{
VStack(alignment: .leading){
Text(i.name)
}
Spacer()
VStack(alignment: .leading){
Image(systemName: "info.circle")
}
}.onTapGesture {
self.exerciseList.append(Exercise(exercise: i.exerciseId, set: 1))
}
}
}
}
}
There is a problem with the line where I set self.$myExercises.workout.exercise. The XCode error shows a different location (a HStack) and gives the following error as it seems unrelated:
Generic parameter 'C0' could not be inferred Info: 1. In call to function 'buildBlock' (SwiftUI.ViewBuilder)
I want to move this the exercise array to the next view to add items to it.
any idea why the above part of the cone is not allowed?
Thanks
The error message was unrelated. The problem was that the let exercise, set: Int was a constant and not a var. changing that solved the issue.
Commenting the part the error raised sometimes help to understand what the compiler is really complaining about.