I am trying to reference a JSON list in the Resources group from a SwiftUI view, but it won't work for some reason. Here is the code in ContentView.swift:
import SwiftUI
struct ContentView: View {
var kitten : Kitten
let stuff = ["this","that","these"]
#State private var i = 0
var body: some View {
VStack{
Text("Placeholder")
Button(action: {
self.i = (self.i+1)%3
// Do something
}) {
Text(stuff[i])
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(kitten:kittens[0])
}
}
and my directory structure looks like this:
Project\
AppleDelegate.swift
SceneDelegateView.swift
ContentView.swift
Models\
Kitten.swift
Resources\
kittens.json
along with the other boiler plate stuff. I thought that if I had kittens.json in the Resources group that I would be able to reference it in the ContentView.swift file, no?
No, you have to load it explicitly using Bundle, like below
String(contentsOf: Bundle.main.url(forResource: stuff[i], withExtension: "json"))
The best way to handle json files is to add them in your Assets, as a Data Set:
1- Open your Assets.xcassets.
2- You create a New Data Set and give it a name like "kittens"
3- Drag & drop your json file into that new Data Set
4- You can access your data like this:
guard let assets = NSDataAsset(name: "kittens") else {
print("Missing data asset: kittens")
}
let kittens = try! JSONDecoder().decode([kitten].self, from: assets.data)
Related
(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.
I'm trying to simplify the ContentView within a project and I'm struggling to understand how to move #State based logic into its own file and have ContentView adapt to any changes. Currently I have dynamic views that display themselves based on #Binding actions which I'm passing the $binding down the view hierarchy to have buttons toggle the bool values.
Here's my current attempt. I'm not sure how in SwiftUI to change the view state of SheetPresenter from a nested view without passing the $binding all the way down the view stack. Ideally I'd like it to look like ContentView.overlay(sheetPresenter($isOpen, $present).
Also, I'm learning SwiftUI so if this isn't the best approach please provide guidance.
class SheetPresenter: ObservableObject {
#Published var present: Present = .none
#State var isOpen: Bool = false
enum Present {
case none, login, register
}
#ViewBuilder
func makeView(with presenter: Present) -> some View {
switch presenter {
case .none:
EmptyView()
case .login:
BottomSheetView(isOpen: $isOpen, maxHeight: UIConfig.Utils.screenHeight * 0.75) {
LoginScreen()
}
case .register:
BottomSheetView(isOpen: $isOpen, maxHeight: UIConfig.Utils.screenHeight * 0.75) {
RegisterScreen()
}
}
}
}
if you don't want to pass $binding all the way down the view you can create a StateObject variable in the top view and pass it with .environmentObject(). and access it from any view with EnvironmentObject
struct testApp: App {
#StateObject var s1: sViewModel = sViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(s1)
}
}
}
You are correct this is not the best approach, however it is a common mistake. In SwiftUI we actually use #State for transient data owned by the view. This means using a value type like a struct, not classes. This is explained at 4:18 in Data Essentials in SwiftUI from WWDC 2020.
EditorConfig can maintain invariants on its properties and be tested
independently. And because EditorConfig is a value type, any change to
a property of EditorConfig, like its progress, is visible as a change
to EditorConfig itself.
struct EditorConfig {
var isEditorPresented = false
var note = ""
var progress: Double = 0
mutating func present(initialProgress: Double) {
progress = initialProgress
note = ""
isEditorPresented = true
}
}
struct BookView: View {
#State private var editorConfig = EditorConfig()
func presentEditor() { editorConfig.present(…) }
var body: some View {
…
Button(action: presentEditor) { … }
…
}
}
Then you just use $editorConfig.isEditorPresented as the boolean binding in .sheet or .overlay.
Worth also taking a look at sheet(item:onDismiss:content:) which makes it much simpler to show an item because no boolean is required it uses an optional #State which you can set to nil to dismiss.
I have created this content view which has a list and it displays companyData struct below.I have thought of reusing the same dropdown view with different data source and navigating back to parent view with selected row data.
import SwiftUI
import Combine
struct Dropdownview: View {
#Environment(\.presentationMode) var presentationMode
#Binding var companyName: String
var companyData = [CompanyData]()
var body: some View {
List(companyData){data in
Button("", action:{
self.companyName = data.name!
self.presentationMode.wrappedValue.dismiss() })
Text(data.name!)
}.navigationBarTitle("Companies")
}
}
struct Dropdownview_Previews: PreviewProvider {
static var previews: some View {
Dropdownview(companyName:.constant(""))
}
}
//environment object class
class AppData: ObservableObject {
#Published var studs : [StudentModel]
}
var body: some View {
VStack{
List(appData.studs,id:\.rollNo){ s in //causing error
Text("\(s.rollNo)")
NavigationLink("", destination: StudentView(s: s))
}
}.navigationBarItems(trailing:
Button(action: {
self.addStud.toggle()
}){
Image(systemName: "plus")
.renderingMode(.original)
}
.sheet(isPresented: $addStud, content: {
AddStudent()
})
)
.navigationBarTitle(Text("Students"),displayMode: .inline)
}
Fatal error: No ObservableObject of type AppData found. A View.environmentObject(_:) for AppData may be missing as an ancestor of this view.
Your sample code is missing some lines at the start of the view. By the sounds of the error message, you already have something like:
struct MyView: View {
#EnvironmentObject var appData: AppData
// ...rest of view ...
}
Alongside that code to get a reference for your object out of the environment, you also need to ensure that, somewhere further up the chain, it's put in. Your error message is telling you that that is where the problem lies – it's looking in the environment for a type of AppData object, but there's nothing in there.
Let's say you declare it the app level; it might look something like this:
#main
struct TestDemoApp: App {
// 1. Instantiate the object, using `#StateObejct` to make sure it's "owned" by the view
#StateObject var appData = AppData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appData) // 2. make it available to the hierarchy of views
}
}
}
What you'll also have to do is make sure that any views that use your environment object also have access to one in their Xcode previews. You might want to create a version of AppData that has example data inside so that your previews don't mess with live data.
extension AppData {
static var preview: AppData = ...
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(AppData.preview)
}
}
I am new in swiftUI, and I know how to do it in Xcode to make two different random "a" and "b",and print the output. However, I do not know how in SwiftUI to show two different "a" and "b".
It seems like if I use var again, it will deny it.
import SwiftUI
struct ContentView: View {
var a=Int.random(in:80...90)
var b=Int.random(in:60...70)
var body: some View {
VStack {
Text ("\(a)+\(b)=")
Text ("\(a)+\(b)=")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You could do something like this:
struct ContentView: View {
var body: some View {
VStack {
Text ("\(Int.random(in:80...90))+\(Int.random(in:80...90))=")
Text ("\(Int.random(in:80...90))+\(Int.random(in:80...90))=")
}
}
}
If you use the same var a and var b for both text fields the same numbers will always appear regardless of how many times you run Int.random