I post the minimum code to reproduce the behavior. Tested on latest macOS and Xcode.
The picker in this example is just a wrapper for the default picker and conforms to Equatable (this is a must to prevent the view from updating when properties doesn't change in the real world view) and categories:
enum Category: Int, Identifiable, CaseIterable {
case one, two, three
var id: Self { self }
var name: String {
switch self {
case .one:
return "First"
case .two:
return "Second"
case .three:
return "Third"
}
}
}
struct CustomPicker: View, Equatable {
static func == (lhs: CustomPicker, rhs: CustomPicker) -> Bool {
lhs.selectedCategory == rhs.selectedCategory
}
#Binding var selectedCategory: Category
var body: some View {
VStack {
Picker("Picker", selection: $selectedCategory) {
ForEach(Category.allCases) { category in
Text(category.name)
}
}
}
}
}
And a simple model to bind to:
final class Model: ObservableObject {
#Published var selectedCategory: Category = .two
}
Now in ContentView:
struct ContentView: View {
#StateObject private var model = Model()
#State private var selectedCategory: Category = .two
var body: some View {
VStack {
Text("Picker bug")
HStack {
CustomPicker(selectedCategory: $selectedCategory)
CustomPicker(selectedCategory: $model.selectedCategory)
}
}
.padding()
}
}
Here is the problem. If I bind the CustomPicker to the #Stateproperty it works as expected. However, if bound to the model's #Published property the value doesn't change when interacting with the control. When not using the Equatable conformance it works as expected.
What's more interesting is that if I change the pickerStyle to something else like segmented or inline it does work again.
Any idea why this happens? Probably a bug?
EDIT:
I found a hack/workaround... the thing is if the CustomPicker is inside a regular TabView it works fine.
TabView {
CustomPicker(selectedCategory: $model.selectedCategory)
.tabItem {
Text("Hack")
}
}
Strange behavior...
Since this only happens with one picker type (and then, only on macOS, iOS is fine), it does look like a bug with this specific picker style. If you add extra logging you can see that for other picker styles, it performs more equality checks, perhaps because there has been user interaction and the other options are visible, so different mechanisms are marking the view as dirty.
When the equality check is happening for this type of picker, it has the new value from the binding for both the left and right hand sides. If you move from a binding to passing the whole model as an observed object, then it works (because it ignores equatable at this level, it seems), but since you're interested in minimising redraws, that's probably not a great solution.
Note that according to the documentation you need to wrap views in EquatableView or use the .equatable() modifier to take advantage of using your own diffing. You might be better off working out where the performance problems you're trying to avoid are coming from, and fixing those instead.
Related
I'm trying to create navigation for my app using Navigation Stack and routing.
My code is functioning and navigating to views, the problem I'm having is that the view is getting called several times from within a switch statement, I have placed the nav stack in the some scene view, then added a simple link, when tapped it goes through the switch statement and picks up the value 3 times and displays the view, I placed a print statement in the switch and it's printed 3 times for my new view value, following on with database calls etc, they are also getting called 3 times.
I'm new to SwiftUI so I'm sure it's user error, any help would be appreciated, thanks.
enum Routing : Hashable {
case AddFlight
case PilotsList
case newview
}
#State var navPath = NavigationPath()
var body: some Scene {
WindowGroup {
NavigationStack (path: $navPath) {
NavigationLink(value: Routing.newview, label: {Text("Go to new view")})
.navigationDestination(for: Routing.self) { route in
switch route {
case .newview:
Text("New View")
let a = print("New view")
case .PilotsList :
PilotsListView()
case .AddFlight:
AddEditFlightView()
}
}
}
}
}
Putting this in an answer because there is a code "fix" for the reprints.
Verified the behavior both in your code and some of my own existing case statements (XCode Version 14.0.1 (14A400)). Additionally the child view's init is called the same number of multiple times, but work in .onAppear() is only called the once. The number of extra calls seems to vary. Have also verified that it happens even when there isn't a case statement but a single possible ChildView.
This makes me think it may have to do with the Layout system negotiating a size for the view. This closure is a #ViewBuilder which we can tell because we can't just put a print statement into it directly. It's being treated as a subview that's negotiating it's size with the parent. Which makes sense in hindsight, but wow, good to know!
This means that items that should only happen once should go in the .onAppear() code of the child view instead of inside of the destination closure which is a #ViewBuilder.
This code is fairly different than yours but that was mostly to check that the effect wasn't some quirk. The key is that it will only do the "onAppear" task once. Note that the perform closure for .onAppear is NOT a ViewBuilder, it is of type () -> Void. It is possible you might prefer .task{} to do an async database call, and that will also just run the once, it looks like.
struct ThreeTimePrintView:View {
#EnvironmentObject var nav:NavigationManager
var body: some View {
NavigationStack (path: $nav.navPath) {
Button("New View") {
nav.navPath.append(Routing.newview)
}.navigationDestination(for: Routing.self) { route in
switch route {
case .newview:
buildNewView().onAppear() { print("New View onAppear") }
case .PilotsList :
Text("Pilot View")
case .AddFlight:
Text("FligtView")
}
}
}
}
func buildNewView() -> some View {
print("New view")
return Text("New View")
}
}
import SwiftUI
enum Routing : Hashable {
case AddFlight
case PilotsList
case newview
}
final class NavigationManager:ObservableObject {
#Published var navPath = NavigationPath()
}
#main
struct ThreePrintCheckerApp: App {
#StateObject var nav = NavigationManager()
var body: some Scene {
WindowGroup {
ThreeTimePrintView().environmentObject(nav)
}
}
}
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.
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.
In the code below, part of a crossword app, I have created a Grid, which contains squares, an #Published array of Squares, and a GridView to display an instance of Grid, griddy. When I change griddy's squares in the GridView, the GridView gets recreated, as expected, and I see an "!" instead of an "A".
Now I've added one more level -- a Puzzle contains an #Published Grid, griddy. In PuzzleView, I therefore work with puzzle.griddy. This doesn't work, though. The letter doesn't change, and a break point in PuzzleView's body never gets hit.
Why? I'm working on an app where I really need these 3 distinct structures.
import SwiftUI
class Square : ObservableObject {
#Published var letter: String
init(letter: String){
self.letter = letter
}
}
class Grid : ObservableObject {
#Published var squares:[[Square]] = [
[Square(letter: "A"), Square(letter: "B"), Square(letter: "C")],
[Square(letter: "D"), Square(letter: "E"), Square(letter: "F"), ]
]
}
class Puzzle: ObservableObject {
#Published var griddy: Grid = Grid()
}
struct GridView: View {
#EnvironmentObject var griddy: Grid
var body: some View {
VStack {
Text("\(griddy.squares[0][0].letter)")
Button("Change Numbers"){
griddy.squares[0][0] = Square(letter:"!")
}
}
}
}
struct PuzzleView: View {
#EnvironmentObject var puzzle: Puzzle
var body: some View {
VStack {
Text("\(puzzle.griddy.squares[0][0].letter)")
Button("Change Numbers"){
puzzle.griddy.squares[0][0] = Square(letter:"!")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
// PuzzleView().environmentObject(Puzzle())
GridView().environmentObject(Grid())
}
}
Technically you could just add this to your Puzzle object:
var anyCancellable: AnyCancellable? = nil
init() {
self.anyCancellable = griddy.objectWillChange.sink(receiveValue: {
self.objectWillChange.send()
})
}
It's a trick I found in another SO post, it'll cause changes in the child object to trigger the parent's objectWillChange.
Edit:
I would like to note that if you change the child object:
puzzle.griddy = someOtherGriddyObject
then you will still be subscribed to the changes of the old griddy object, and you won't receive new updates.
You can probably get by this by just updating the cancellable after changing the object:
puzzle.griddy = someOtherGriddyObject
puzzle.anyCancellable = puzzle.griddy.objectWillChange.sink(receiveValue: {
puzzle.objectWillChange.send()
})
Think in this direction: in MVVM SwiftUI concept every introduced ObservableObject entity must be paired with corresponding view ObservedObject (EnvironmentObject is the same as ObservedObject, just with different injection mechanism), so specific change in ViewModel would refresh corresponding View, there is no other magic/automatic propagations of changes from model to view.
So if Square is ObservableObject, there should be some SquareView, as
struct SquareView {
#ObservedObject var vm: Square
...
so if you Puzzle observable includes Grid observable, then corresponding PuzzleView having observed puzzle should include GridView having observed grid, which should include SqareView having observed square.
Ie.
struct PuzzleView: View {
#EnvironmentObject var puzzle: Puzzle
var body: some View {
GridView().environmentObject(puzzle.griddy)
}
}
// ... and same for GridView, and so on...
This approach result in very optimal update, because modifying one Square makes only one corresponding SquareView refreshed.
But if you accumulate all Squares.objectWillChange in one container update, then modifying one Square result in total UI update, that is bad and for UI fluency and for battery.
I'd say because Grid is reference type, and #Published is triggered when griddy is mutated.
class Puzzle: ObservableObject {
#Published var griddy: Grid = Grid()
}
Note that griddy does not actually mutate since it is a reference.
But why does it work in #EnvironmentObject var griddy: Grid?
My guess is that SDK implicitly observes publishers in an #ObservableObject.
But maybe someone can provide better detail answer.
I personally would avoid long publisher chain, and just flatten the nested structure.
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.