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.
Related
Cow you give me some confirmation about my understanding about #ObservedObject and #EnvironmentObject?
In my mind, using an #ObservedObject is useful when we send data "in line" between views that are sequenced, just like in "prepare for" in UIKit while using #EnvironmentObject is more like "singleton" in UIKit. My question is, is my code making the right use of these two teniques? Is this the way are applied in real development?
my model used as brain for funcions (IE urls sessions, other data manipulations)
class ModelClass_ViaObservedObject: ObservableObject {
#Published var isOn: Bool = true
}
class ModelClass_ViaEnvironment: ObservableObject {
#Published var message: String = "default"
}
my main view
struct ContentView: View {
//way to send data in views step by step
#StateObject var modelClass_ViaObservedObject = ModelClass_ViaObservedObject()
//way to share data more or less like a singleton
#StateObject var modelClass_ViaEnvironment = ModelClass_ViaEnvironment()
var myBackgroundColorView: Color {
if modelClass_ViaObservedObject.isOn {
return Color.green
} else {
return Color.red
}
}
var body: some View {
NavigationView {
ZStack {
myBackgroundColorView
VStack {
NavigationLink(destination:
SecondView(modelClass_viaObservedObject: modelClass_ViaObservedObject)
) {
Text("Go to secondary view")
.padding()
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(.black, lineWidth: 1)
)
}
Text("text received from second view: \(modelClass_ViaEnvironment.message)")
}
}
.navigationTitle("Titolo")
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(modelClass_ViaEnvironment)
}
}
my second view
struct SecondView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var modelClass_viaObservedObject: ModelClass_ViaObservedObject
//global data in environment, not sent step by step view by view
#EnvironmentObject var modelClass_ViaEnvironment: ModelClass_ViaEnvironment
var body: some View {
VStack(spacing: 5) {
Text("Second View")
Button("change bool for everyone") {
modelClass_viaObservedObject.isOn.toggle()
dismiss()
}
TextField("send back", text: $modelClass_ViaEnvironment.message)
Text(modelClass_ViaEnvironment.message)
}
}
}
No, we use #State for view data like if a toggle isOn, which can either be a single value itself or a custom struct containing multiple values and mutating funcs. We pass it down the View hierarchy by declaring a let in the child View or use #Binding var if we need write access. Regardless of if we declare it let or #Binding whenever a different value is passed in to the child View's init, SwiftUI will call body automatically (as long as it is actually accessed in body that is).
#StateObject is for when a single value or a custom struct won't do and we need a reference type instead for view data, i.e. if persisting or syncing data (not using the new async/await though because we use .task for that). The object is init before body is called (usually before it is about to appear) and deinit when the View is no longer needed (usually after it disappears).
#EnvironmentObject is usually for the store object that holds model structs in #Published properties and is responsible for saving or syncing,. The difference is the model data is not tied to any particular View, like #State and #StateObject are for view data. This object is usually a singleton, one for the app and one with sample data for when previewing, because it should never be deinit. The advantage of #EnvironmentObject over #ObservedObject is we don't need to pass it down through each View as a let that don't need the object when we only need it further down the hierarchy. Note the reason it has to be passed down as a let and not #ObservedObject is then body would be needlessly called in the intermediate Views because SwiftUI's dependency tracking doesn't work for objects only value types.
Here is some sample code:
struct MyConfig {
var isOn = false
var message = ""
mutating func reset() {
isOn = false
message = ""
}
}
struct MyView: View {
#State var config = MyConfig() // grouping vars into their struct makes use of value semantics to track changes (a change to any of its properties is detected as a change to the struct itself) and offers testability.
var body: some View {
HStack {
ViewThatOnlyReads(config: config)
ViewThatWrites(config: $config)
}
}
}
struct ViewThatOnlyReads: View {
let config: MyConfig
var body: some View {
Text(config.isOn ? "It's on" : "It's off")
}
}
struct ViewThatWrites: View {
#Binding var config: MyConfig
var body: some View {
Toggle("Is On", isOn: $config.isOn)
}
}
Is it possible to extract logic that depends on the SwiftUI environment outside of the views?
For example, consider the scenario where you have a Theme struct that computes a color depending on a property in the environment, something akin to the sample code below.
What I'd like to do is extract out the logic that computes a color so that it can be used in multiple places. Ideally I'd like to use #Environment in the Theme struct so that I only have to retrieve the value it in once place - the alternative is that I retrieve from the environment at the call site of my Theme computation and inject the value in. That alternative works fine, but I'd like to avoid the need to retrieve the environment value all over the place.
/// A structure to encapsulate common logic, but the logic depends on values in the environment.
struct Theme {
#Environment(\.isEnabled) var isEnabled: Bool
var color: Color {
isEnabled ? .blue : .gray
}
}
/// One of many views that require the logic above
struct MyView: View {
let theme: Theme
var body: some View {
theme.color
}
}
/// A little app to simulate the environment values changing
struct MyApp: App {
#State var disabled: Bool
var body: some Scene {
WindowGroup {
VStack {
Toggle("Disabled", isOn: $disabled)
MyView(theme: Theme())
.disabled(disabled)
}
}
}
}
The Sample code above doesn't work, ie if you toggle the switch in the app the View's color does not change. This sample code only serves to show how I'd ideally like it to work, particularly because it doesn't require me to litter #Environment throughout MyView and similar views just to retrieve the value and pass it into a shared function.
One thing that I thought could be causing the problem is that the Theme is created outside of the scope where the Environment is changing, but if I construct a Theme inside MyView the behaviour doesn't change.
My confusion here indicates that I'm missing something fundamental in my understanding of the SwiftUI Environment. I'd love to understand why that sample code doesn't work. If Theme were a View with the color logic in its body, it would be updating, so why doesn't it cause an update in it's current setup?
The approach should be different, view parts in views, model parts in models, "separate and rule"
struct Theme { // << view layer independent !!
func color(for enabled: Bool) -> Color { // << dependency injection !!
enabled ? .blue : .gray
}
}
struct MyView: View {
#Environment(\.isEnabled) var isEnabled: Bool
let theme: Theme
var body: some View {
theme.color(for: isEnabled) // << here !!
}
}
I feel like you're mixing a few things, so let me tell you how I'd structure this.
First of all, I don't think a theme should have state, it should be a repository of colors.
struct Theme {
var enabledColor: Color = .blue
var disabledColor: Color = .gray
}
Your View is where you should have your state
struct MyView: View {
#Environment(\.isEnabled) var isEnabled: Bool
var body: some View {
// ??
}
}
What I would suggest is that you create your own EnvironmentKey and inject your theme into the environment:
struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = .init()
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
Now your View can use the environment to read the theme. I like to keep my views light of logic, but presentation logic in them makes sense to me.
struct MyView: View {
#Environment(\.theme) var theme
#Environment(\.isEnabled) var isEnabled: Bool
var body: some View {
isEnabled ? theme.enabledColor : theme.disabledColor
}
}
You'll need to inject your theme at some point in your app, you should add it near the top of the view hierarchy to make sure all views get access to it.
struct MyApp: App {
#State var disabled: Bool
#State var theme: Theme = .init()
var body: some Scene {
WindowGroup {
VStack {
Toggle("Disabled", isOn: $disabled)
MyView()
.disabled(disabled)
}
.environment(\.theme, theme)
}
}
}
I wrote this article about using the environment to provide values to the view hierarchy, you may find it useful.
Simple sample code with toggle button (slightly modified from hackingwithswift:
This code(hackingwithswift original and my version) IS redrawing every list cell whenever any toggle happens. I modified code to better debug view drawing.
import SwiftUI
struct User: Identifiable {
let id = UUID()
var name: String
var isContacted = false
}
struct ProfileView: View {
#State private var users = [
User(name: "Taylor"),
User(name: "Justin"),
User(name: "Adele")
]
var body: some View {
let _ = Self._printChanges()
List($users) { $user in
ProfileCell(user: $user)
}
}
}
struct ProfileCell: View{
#Binding var user: User
var body: some View{
let _ = Self._printChanges()
Text(user.name)
Spacer()
Toggle("User has been contacted", isOn: $user.isContacted)
.labelsHidden()
}
}
Running app and toggling will print following in console for every toggle:
ProfileView: _users changed.
ProfileCell: #self, _user changed.
ProfileCell: #self, _user changed.
ProfileCell: #self, _user changed.
Hackingwithswift tutorial states "Using a binding in this way is the most efficient way of modifying the list, because it won’t cause the entire view to reload when only a single item changes.", however that does not seem to be true.
Is it possible to redraw only item that was changed?
Theoretically it should be working, but it seems they changed something since first introduction, because now on state change they recreate(!) bindings (all of them), so automatic view changes handler interpret that as view update (binding is a property after all).
A possible workaround for this is to help rendering engine and check view equitability manually.
Tested with Xcode 13.4 / iOS 15.5
Main parts:
// 1
List($users) { $user in
EquatableView(content: ProfileCell(user: $user)) // << here !!
}
// 2
struct ProfileCell: View, Equatable {
static func == (lhs: ProfileCell, rhs: ProfileCell) -> Bool {
lhs.user == rhs.user
}
// ...
// 3
struct User: Identifiable, Equatable {
Test module is here
I basically have the same code as in this question. The problem I have is that when the tapGesture event happens, the sheet shows (the sheet code is called) but debug shows that showUserEditor is false (in that case, how is the sheet showing...?) and that selectedUserId is still nil (and therefore crashes on unwrapping it...)
The view:
struct UsersView: View {
#Environment(\.managedObjectContext)
private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
animation: .default)
private var users: FetchedResults<User>
#State private var selectedUserId : NSManagedObjectID? = nil
#State private var showUserEditor = false
var body: some View {
NavigationView {
List {
ForEach(users) { user in
UserRowView(user: user)
.onTapGesture {
self.selectedUserId = user.objectID
self.showUserEditor = true
}
}
}
}.sheet(isPresented: $showUserEditor) {
UserEditorView(userId: self.selectedUserId!)
}
}
}
If you want, I can publish the editor and the row but they seem irrelevant to the question as the magic should happen in the view.
So, I still haven't figured out WHY the code posted in the question didn't work, with a pointer from #loremipsum I got a working code by using another .sheet() method, one that takes an optional Item and not a boolean flag. The code now looks like this and works, but still if anyone can explain why the posted code didn't work I'd appreciate it.
struct UsersView: View {
#Environment(\.managedObjectContext)
private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
animation: .default)
private var users: FetchedResults<User>
#State private var selectedUser : User? = nil
var body: some View {
NavigationView {
List {
ForEach(users) { user in
UserRowView(user: user)
.onTapGesture {
self.selectedUser = user
}
}.onDelete(perform: deleteItems)
}
}.sheet(item: $selectedUser, onDismiss: nil) { user in
UserEditorView(user: user)
}
}
}
struct == immutable and SwiftUI decides when the struct gets init and reloaded
Working with code that depends on SwiftUI updating non-wrapped variables at a very specific time is not recommended. You have no control over this process.
To make your first setup work you need to use SwiftUI wrappers for the variables
.sheet(isPresented: $showUserEditor) {
//struct == immutable SwiftUI wrappers load the entire struct when there are changes
//With your original setup this variable gets created/set when the body is loaded so the orginal value of nil is what is seen in the next View
UserEditorView1(userId: $selectedUserId)
}
struct UserEditorView1: View {
//This is what you orginal View likely looks like it won't work because of the struct being immutable and SwiftUI controlling when the struct is reloaded
//let userId: NSManagedObjectID? <---- Depends on specific reload steps
//To make it work you would use a SwiftUI wrapper so the variable gets updated when SwiftUI descides to update it which is invisible to the user
#Binding var userId: NSManagedObjectID?
//This setup though now requres you to go fetch the object somehow and put it into the View so you can edit it.
//It is unnecessary though because SwiftUI provides the .sheet init with item where the item that is set gets passed directly vs waiting for the SwiftUi update no optionals
var body: some View {
Text(userId?.description ?? "nil userId")
}
}
Your answer code doesn't work because your parameter is optional and Binding does not like optionals
struct UserEditorView2: View {
//This is the setup that you posted in the Answer code and it doesn't work becaue of the ? Bindings do not like nil. You have to create wrappers to compensate for this
//But unecessary because all CoreData objects are ObservableObjects so you dont need Binding here the Binding is built-in the object for editing the variables
#Binding var user: User?
var body: some View {
TextField("nickname", text: $user.nickname)
}
}
Now for working code with an easily editable CoreData Object
struct UsersView: View {
#Environment(\.managedObjectContext)
private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
animation: .default)
private var users: FetchedResults<User>
//Your list view would use the CoreData object to trigger a sheet when the new value is available. When nil there will not be a sheet available for showing
#State private var selectedUser : User? = nil
var body: some View {
NavigationView {
List {
ForEach(users) { user in
UserRowView(user: user)
.onTapGesture {
self.selectedUser = user
}
}
}
}.sheet(item: $selectedUser, onDismiss: nil) { user in //This gives you a non-optional user so you don't have to compensate for nil in the next View
UserEditorView3(user: user)
}
}
}
Then the View in the sheet would look like this
struct UserEditorView3: View {
//I mentioned the ObservedObject in my comment
#ObservedObject var user: User
var body: some View {
//If your nickname is a String? you have to compensate for that optional but it is much simpler to do it from here
TextField("nickname", text: $user.nickname.bound)
}
}
//This comes from another very popular SO question (couldn't find it to quote it) that I could not find and is necessary when CoreData does not let you define a variable as non-optional and you want to use Binding for editing
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
public var bound: String {
get {
//This just give you an empty String when the variable is nil
return _bound ?? ""
}
set {
_bound = newValue.isEmpty ? nil : newValue
}
}
}
I'm new to SwiftUI and understand that I may need to implement EnvironmentObject in some way, but I'm not sure how in this case.
This is the Trade class
class Trade {
var teamsSelected: [Team]
init(teamsSelected: [Team]) {
self.teamsSelected = teamsSelected
}
}
This is the child view. It has an instance trade from the Trade class. There is a button that appends 1 to array teamsSelected.
struct TeamRow: View {
var trade: Trade
var body: some View {
Button(action: {
self.trade.teamsSelected.append(1)
}) {
Text("Button")
}
}
}
This is the parent view. As you can see, I pass trade into the child view TeamRow. I want trade to be in sync with trade in TeamRow so that I can then pass trade.teamsSelected to TradeView.
struct TeamSelectView: View {
var trade = Trade(teamsSelected: [])
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: TradeView(teamsSelected: trade.teamsSelected)) {
Text("Trade")
}
List {
ForEach(teams) { team in
TeamRow(trade: self.trade)
}
}
}
}
}
}
I've taken your code and changed some things to illustrate how SwiftUI works in order to give you a better understanding of how to use ObservableObject, #ObservedObject, #State, and #Binding.
One thing to mention up front - #ObservedObject is currently broken when trying to run SwiftUI code on a physical device running iOS 13 Beta 6, 7, or 8. I answered a question here that goes into that in more detail and explains how to use #EnvironmentObject as a workaround.
Let's first take a look at Trade. Since you're looking to pass a Trade object between views, change properties on that Trade object, and then have those changes reflected in every view that uses that Trade object, you'll want to make Trade an ObservableObject. I've added an extra property to your Trade class purely for illustrative purposes that I'll explain later. I'm going to show you two ways to write an ObservableObject - the verbose way first to see how it works, and then the concise way.
import SwiftUI
import Combine
class Trade: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
var name: String {
willSet {
self.objectWillChange.send()
}
}
var teamsSelected: [Int] {
willSet {
self.objectWillChange.send()
}
}
init(name: String, teamsSelected: [Int]) {
self.name = name
self.teamsSelected = teamsSelected
}
}
When we conform to ObservableObject, we have the option to write our own ObservableObjectPublisher, which I've done by importing Combine and creating a PassthroughSubject. Then, when I want to publish that my object is about to change, I can call self.objectWillChange.send() as I have on willSet for name and teamsSelected.
This code can be shortened significantly, however. ObservableObject automatically synthesizes an object publisher, so we don't actually have to declare it ourselves. We can also use #Published to declare our properties that should send a publisher event instead of using self.objectWillChange.send() in willSet.
import SwiftUI
class Trade: ObservableObject {
#Published var name: String
#Published var teamsSelected: [Int]
init(name: String, teamsSelected: [Int]) {
self.name = name
self.teamsSelected = teamsSelected
}
}
Now let's take a look at your TeamSelectView, TeamRow, and TradeView. Keep in mind once again that I've made some changes (and added an example TradeView) just to illustrate a couple of things.
struct TeamSelectView: View {
#ObservedObject var trade = Trade(name: "Name", teamsSelected: [])
#State var teams = [1, 1, 1, 1, 1]
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: TradeView(trade: self.trade)) {
Text(self.trade.name)
}
List {
ForEach(self.teams, id: \.self) { team in
TeamRow(trade: self.trade)
}
}
Text("\(self.trade.teamsSelected.count)")
}
.navigationBarItems(trailing: Button("+", action: {
self.teams.append(1)
}))
}
}
}
struct TeamRow: View {
#ObservedObject var trade: Trade
var body: some View {
Button(action: {
self.trade.teamsSelected.append(1)
}) {
Text("Button")
}
}
}
struct TradeView: View {
#ObservedObject var trade: Trade
var body: some View {
VStack {
Text("\(self.trade.teamsSelected.count)")
TextField("Trade Name", text: self.$trade.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
}
Let's first look at #State var teams. We use #State for simple value types - Int, String, basic structs - or collections of simple value types. #ObservedObject is used for objects that conform to ObservableObject, which we use for data structures that are more complex than just Int or String.
What you'll notice with #State var teams is that I've added a navigation bar item that will append a new item to the teams array when pressed, and since our List is generated by iterating through that teams array, our view re-renders and adds a new item to our List whenever the button is pressed. That's a very basic example of how you would use #State.
Next, we have our #ObservedObject var trade. You'll notice that I'm not really doing anything different than you were originally. I'm still creating an instance of my Trade class and passing that instance between my views. But since it's now an ObservableObject, and we're using #ObservedObject, our views will now all receive publisher events whenever the Trade object changes and will automatically re-render their views to reflect those changes.
The last thing I want to point out is the TextField I created in TradeView to update the Trade object's name property.
TextField("Trade Name", text: self.$trade.name)
The $ character indicates that I'm passing a binding to the text field. This means that any changes TextField makes to name will be reflected in my Trade object. You can do the same thing yourself by declaring #Binding properties that allow you to pass bindings between views when you are trying to sync state between your views without passing entire objects.
While I changed your TradeView to take #ObservedObject var trade, you could simply pass teamsSelected to your trade view as a binding like this - TradeView(teamsSelected: self.$trade.teamsSelected) - as long as your TradeView accepts a binding. To configure your TradeView to accept a binding, all you would have to do is declare your teamsSelected property in TradeView like this:
#Binding var teamsSelected: [Team]
And lastly, if you run into issues with using #ObservedObject on a physical device, you can refer to my answer here for an explanation of how to use #EnvironmentObject as a workaround.
You can use #Binding and #State / #Published in Combine.
In other words, use a #Binding property in Child View and bind it with a #State or a #Published property in Parent View as following.
struct ChildView: View {
#Binding var property1: String
var body: some View {
VStack(alignment: .leading) {
TextField(placeholderTitle, text: $property1)
}
}
}
struct PrimaryTextField_Previews: PreviewProvider {
static var previews: some View {
PrimaryTextField(value: .constant(""))
}
}
struct ParentView: View{
#State linkedProperty: String = ""
//...
ChildView(property1: $linkedProperty)
//...
}
or if you have a #Publilshed property in your viewModel(#ObservedObject), then use it to bind the state like ChildView(property1: $viewModel.publishedProperty).
firstly thanks a lot to graycampbell for giving me a better understanding! However, my understanding does not seem to work out completely. I have a slightly different case which I'm not fully able to solve.
I've already asked my question in a separate thread, but I want to add it here as well, because it somehow fits the topic: Reading values from list of toggles in SwiftUI
Maybe somebody of you guys can help me with this. The main difference to the initial post if this topic is, that I have to collect Data from each GameGenerationRow in the GameGenerationView and then hand it over to another View.