I am facing issues setting up Binding in the following SwiftUI code snippet. I am trying this on xCode Beta 7 ((11M392r).
In the code snippet below, I am creating 2 Stepper views.
If I pass $student.totalMarks to Stepper, it works and creates the right Binding.
But if I try to access $student.marks.score1, that does not work and shows the following compilation error:
Generic parameter 'Subject' could not be inferred.
Is there a way to pass single field from a nested property into a binding?
struct Marks {
public let score1: Int
public let score2: Int
public let score3: Int
}
class Student: ObservableObject {
#Published var totalMarks: Int = 145
#Published var marks = Marks(score1: 67, score2: 56, score3: 64)
}
struct ContentView: View {
#ObservedObject var student = Student()
var body: some View {
return VStack {
Stepper("Total Score: \(student.totalMarks)", value: $student.totalMarks)
Stepper("Score 1: \(student.marks.score1)", value: $student.marks.score1)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Easy. ;-) Don't use a constant for a stepper's value binding. Rather make your scores variables (using var instead of let in struct Marks).
Related
How can I refresh an environment var in SwiftUI? It is easy to update any object that's a part of an environment object, but it seems like there should be a way to re-initialize.
struct reinitenviron: View{
#EnvironmentObject private var globalObj: GlobalClass
var body: some View{
Text("refresh").onTapGesture {
globalObj = GlobalClass() //error here
}
}
}
The following gives an error that globalObj is get only. Is it possible to re-initialize?
A possible solution is to introduce explicit method in GlobalClass to reset it to initial state and use that method and in init and externally, like
class GlobalClass: ObservableObject {
#Published var value: Int = 1
init() {
self.reset()
}
func reset() {
self.value = 1
// do other activity if needed
}
}
struct reinitenviron: View{
#EnvironmentObject private var globalObj: GlobalClass
var body: some View{
Text("refresh").onTapGesture {
globalObj.reset() // << here
}
}
}
(You can skip this part and just look at the code.) I'm creating a complicated form. The form creates, say, a Post object, but I want to be able to create several Comment objects at the same time. So I have a Post form and a Comment form. In my Post form, I can fill out the title, description, etc., and I can add several Comment forms as I create more comments. Each form has an #ObservedObject viewModel of its own type. So I have one parent Post #ObservedObject viewModel, and another #ObservedObject viewModel for the array of the Comment objects which is also a #ObservedObject viewModel.
I hope that made some sense -- here is code to minimally reproduce the issue (unrelated to Posts/Comments). The objective is to make the count of the "Childish" viewModels at the parent level count up like how they count up for the "Child" view.
import Combine
import SwiftUI
final class ParentScreenViewModel: ObservableObject {
#Published var childScreenViewModel = ChildScreenViewModel()
}
struct ParentScreen: View {
#StateObject private var viewModel = ParentScreenViewModel()
var body: some View {
Form {
NavigationLink(destination: ChildScreen(viewModel: viewModel.childScreenViewModel)) {
Text("ChildishVMs")
Spacer()
Text("\(viewModel.childScreenViewModel.myViewModelArray.count)") // FIXME: this count is never updated
}
}
}
}
struct ParentScreen_Previews: PreviewProvider {
static var previews: some View {
ParentScreen()
}
}
// MARK: - ChildScreenViewModel
final class ChildScreenViewModel: ObservableObject {
#Published var myViewModelArray: [ChildishViewModel] = []
func appendAnObservedObject() {
objectWillChange.send() // FIXME: does not work
myViewModelArray.append(ChildishViewModel())
}
}
struct ChildScreen: View {
#ObservedObject private var viewModel: ChildScreenViewModel
init(viewModel: ChildScreenViewModel = ChildScreenViewModel()) {
self.viewModel = viewModel
}
var body: some View {
Button {
viewModel.appendAnObservedObject()
} label: {
Text("Append a ChildishVM (current num: \(viewModel.myViewModelArray.count))")
}
}
}
struct ChildScreen_Previews: PreviewProvider {
static var previews: some View {
ChildScreen()
}
}
final class ChildishViewModel: ObservableObject {
#Published var myProperty = "hey!"
}
ParentView:
ChildView:
I can't run this in previews either -- seems to need to be run in the simulator. There are lots of questions similar to this one but not quite like it (e.g. the common answer of manually subscribing to the child's changes using Combine does not work). Would using #EnvironmentObject help somehow? Thanks!
First get rid of the view model objects, we don't use those in SwiftUI. The View data struct is already the model for the actual views on screen e.g. UILabels, UITables etc. that SwiftUI updates for us. It takes advantage of value semantics to resolve consistency bugs you typically get with objects, see Choosing Between Structures and Classes. SwiftUI structs uses property wrappers like #State to make these super-fast structs have features like objects. If you use actual objects on top of the View structs then you are slowing down SwiftUI and re-introducing the consistency bugs that Swift and SwiftUI were designed to eliminate - which seems to me is exactly the problem you are facing. So it of course is not a good idea to use Combine to resolve consistency issues between objects it'll only make the problem worse.
So with that out of the way, you just need correct some mistakes in your design. Model types should be structs (these can be arrays or nested structs) and have a single model object to manage the life-cycle and side effects of the struct. You can have structs within structs and use bindings to pass them between your Views when you need write access, if you don't then its simply a let and SwiftUI will automatically call body whenever a View is init with a different let from last time.
Here is a basic example:
struct Post: Identifiable {
let id = UUID()
var text = ""
}
class Model: ObservableObject {
#Published var posts: [Post] = []
// func load
// func save
// func delete a post by ID
}
struct ModelController {
static let shared = ModelController()
let model = Model()
//static var preview: ModelController {
// ...
//}()
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(ModelController.shared.model)
}
}
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
ForEach($model.posts) { $post in
ContentView2(post: post)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(ModelController.shared.preview)
}
}
struct ConventView2: View {
#Binding var post: Post
var body: some View {
TextField("Enter Text", text: $post.text)
}
}
For a more detail check out Apple's Fruta and Scrumdinger samples.
In many cases in SwiftUI, values are marked with a $ to indicate that they’re a Binding variable, and allow them to update dynamically. Here’s an example of this behavior:
class Car: ObservableObject {
#Published var isReadyForSale = true
}
struct SaleView: View {
#Binding var isOn: Bool
var body: some View {
Toggle("Ready for Sale", isOn: $isOn)
}
}
struct ContentView: View {
#ObservedObject var car: Car
var body: some View {
Text("Details")
.font(.headline)
SaleView(isOn: $car.isReadyForSale) // generates a Binding to 'isReadyForSale' property
}
}
The $ is used twice to allow the Toggle to change whether the car is ready for sale, and for the car’s status to update the Toggle.
However, some values seem to update without the $. For instance, in this tutorial about different property wrappers, they show the following example:
class TestObject: ObservableObject {
#Published var num: Int = 0
}
struct ContentView: View {
#StateObject var stateObject = TestObject()
var body: some View {
VStack {
Text("State object: \(stateObject.num)")
Button("Increase state object", action: {
stateObject.num += 1
print("State object: \(stateObject.num)")
})
}
.onChange(of: stateObject.num) { newStateObject in
print("State: \(newStateObject)")
}
}
}
Why does it use Text("State object: \(stateObject.num)") and not Text("State object: \($stateObject.num)") with a dollar sign prefix? It was my understanding when you wanted a view to automatically update when a variable it uses changes, you prefix it with a dollar sign. Is that wrong?
I have a enum that defines the language index. I put this enum in a file named Language.swift
enum Language: Int {
case en
case zh_hant
}
I have tried to declare a global variable that stores the current language.
final class ModelData: ObservableObject {
#Published var currentLanguage: Language = Language.zh_hant
#Published var globalString: [String] = load("strings.json")
}
However, when I tried to access that, I have the following error:
struct HomeView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
Text("\(modelData.currentLanguage.rawValue)") // error, see the screen capture
Text("\(modelData.globalString.count)") // no problem
}
}
Yet, I used the same way to access the array, there is no problem.
The above error can be resolved by moving enum Language to be in the same file as class ModelData.
Yet, another problem was then identified.
I tried to do this in my code:
var languageIndex: Int {
modelData.currentLanguage.rawValue
}
var body: some View {
Text("\(modelData.globalString[languageIndex])") // preview cause "updating took more than 5 seconds]
}
My global String like this
["Hello", "你好"]
The problem appears in the Canvas view on preview the UI.
Yet, it seems to work fine under simulator. Any idea?
The problem appears in the Canvas view on preview the UI. Yet, it seems to work fine under simulator.
You have to set the environment object in the preview
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
.environmentObject(ModelData())
}
}
I was looking for some Code (SwiftUI) to get a JSON File from a website into my Xcode Project, and I found a good Sample but when I try to change the Code and wanna use a 'var' from one struct in an other struct.
struct Course: Decodable, Identifiable, Hashable {
let id: Int
let name: String
var link: String
let imageUrl: String
let number_of_lessons: Int
}
class NetworkManager: ObservableObject {
#Published var courses = [Course]()
func getAllCourses() {
guard let url = URL(string: "https://api.letsbuildthatapp.com/jsondecodable/courses") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
do {
let courses = try JSONDecoder().decode([Course].self, from: data!)
DispatchQueue.main.async {
self.courses = courses
print(courses)
}
} catch {
print("Failed To decode: ", error)
}
}.resume()
}
init() {
getAllCourses()
}
}
struct SwiftUIView: View {
var cs:Course
var body: some View {
Text(cs.name)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView() //here it says Missing argument for parameter...
}
Xcode says: Missing argument for parameter 'cs' in call, Insert 'cs:<#Course#>'.
As you are trying to initialize a struct, you need to pass all properties with setters, here that is cs, as body is a get-only property.
You will need to pass a Course object via SwiftUIView(cs: course). This course can be just a static course, as it's only used in your SwiftUI preview.
Swift autogenerates initializers for structs, so your SwiftUIView has one like this:
public init(cs: Course) {
self.cs = cs
}
So in order to create an instance of your SwiftUIView in the previews you have to declare it like this:
struct ContentView_Previews: PreviewProvider {
static let cs = Course(id: 7,
name: "The best course",
link: "thebestcourse.com",
imageUrl: "thebestcourse.com/image.jpg",
number_of_lessons: 5)
static var previews: some View {
SwiftUIView(cs: cs)
}
}