The index/count work you're doing should already by handled by the ForEach view. In general, rather than having the Picker operate on indices, have it operate on the values themselves:@State var orderStatus: String
private static var orderStatusFromArray = ["005", "010", "022", "025", "030", "035", "040", "045", "046", "060"]
var body: some View {
Picker("From status", selection: $orderStatus) {
ForEach(Self.orderStatusFromArray, id: \.self) { status in
Text(status)
}
}
}
private func saveStatus() {
UserDefaults.standard.set(orderStatus, forKey: "Ordeview.orderHeaderFilter.fromStatusStatus")
}The tag behavior and the indices are all internal and up to the Picker to manage, because they technically only matter to the generation of the picker view itself. As far as you're concerned, you have a list of values and the user should pick one. Ultimately there's something like an implicit tag added to your Text view by the ForEach: it'll be tagged with the value of 'status' in this case.This is a common enough situation that you'll often see enumerations used for this purpose. Consider:enum OrderStatus: String, CaseIterable, Identifiable {
case s05 = "005"
case s10 = "010"
case s22 = "022"
case s25 = "025"
case s30 = "030"
case s35 = "035"
case s40 = "040"
case s45 = "045"
case s46 = "046"
case s60 = "060"
var id: String { rawValue }
}
@State var orderStatus: OrderStatus
init() {
// Restore from defaults
if let saved = UserDefaults.standard.string(forKey: "view.orderHeaderFilter.fromStatus") {
self.orderStatus = OrderStatus(rawValue: saved) ?? .v05
} else {
self.orderStatus = .v05
}
}
var body: some View {
Picker("From Status", selection: $orderStatus) {
ForEach(OrderStatus.allCases) { status in
Text(status.rawValue)
}
}
}
private func saveStatus() {
UserDefaults.standard.set(orderStatus.rawValue, forKey: "view.orderHeaderFilter.fromStatus")
}In this instance, you have a nice representation for your code to use (the OrderStatus enum) and a handy textual variant for reading/writing to the user defaults.In fact, why not go a little further? Wouldn't it be nice if your @State variable was simply backed by UserDefaults directly, and vended an appropriate Binding when needed? Look no further:@propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
init(_ key: String, defaultValue: Value) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: Value {
get { Self.getWrapped(forKey: key, defaultValue: defaultValue) }
mutating set { Self.setWrapped(value: newValue, forKey: key) }
}
var projectedValue: Binding<Value> {
Binding(get: { Self.getWrapped(forKey: self.key, defaultValue: self.defaultValue) },
set: { Self.setWrapped(value: $0, forKey: self.key) })
}
static private func getWrapped(forKey key: String, defaultValue: Value) -> Value {
UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
}
static private func setWrapped(value: Value, forKey key: String) {
UserDefaults.standard.set(value, forKey: key)
}
}With this, you'll have a significantly simpler implementation:@UserDefault("view.orderHeaderFilter.fromStatus", OrderStatus.v05)
private var orderStatus: OrderStatus
var body: some View {
Picker("From Status", selection: $orderStatus) {
ForEach(OrderStatus.allCases) { status in
Text(status.rawValue)
}
}
}Saving to and loading from UserDefaults is now built into the property wrapper.
Post
Replies
Boosts
Views
Activity
Essentially what you're looking to do is to reverse the 'hasError' property after a certain amount of time. There are several ways to do this, the simplest being a basic dispatch:DispatchQueue.main.async { self.vm.hasError = false }A more robust solution would be to use a Publisher though, via a PassthroughSubject. That will let you set delays and even debounce the events (coalescing nearby events together). For a true 'flash' effect, I'd recommend setting the background color to red instantly, without animation, and then animating the fade back to the original color (or to Clear, which is what a TextField would use normally).Here's my implementation of a ContentView that would work with your Person class to implement a decent-looking background flash:struct ContentView: View {
@ObservedObject var vm = Person()
@State var color = Color.clear
private var unflash = PassthroughSubject<Void, Never>()
private var afterUnflash: AnyPublisher<Void, Never>
init() {
self.afterUnflash = unflash
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
var body: some View {
VStack {
TextField("Name", text: $vm.name)
.padding()
.background(color)
.onReceive(vm.$hasError, perform: {
if $0 {
self.color = .red
self.unflash.send(())
} else {
withAnimation(.linear(duration: 0.2)) {
self.color = .clear
}
}
})
.onReceive(afterUnflash, perform: { _ in
self.vm.hasError = false
})
}
}
}I've added a Color state value which I'm using to set the background, followed by a PassthroughSubject and a debouncing publisher derived from that. I then monitor the publisher for the vm.hasError property, and when the value becomes true I set the color to red and trigger my PassthroughSubject; when the value becomes false I use an animation to set the color back to clear. Lastly, I monitor the debounced publisher and set vm.hasError to false when it fires. The debouncing here is used to coalesce multiple signals together; try commenting out line ten and typing a series of "!" and you'll see the color switching back & forth rapidly. With the debounce in there, it will only send the signal on when 300 milliseconds of time has elapsed since the last one was received, giving a better overall appearance—it'll stay red while you quickly type "!" and fade out only once you let go.
There's a `.onInsert(of:perform:)` view modifier on DynamicContentView (to which only ForEach currently conforms) that does this. You pass in an array of accepted type identifiers, and then your block is called. It's passed the index on which the drop occurred (within the content of the ForEach) and an array of NSItemProvider instances.Here's an iPad app which demonstrates its use:fileprivate let dataUTI = "data.raw.dragdata"
fileprivate final class DragData: NSObject, NSItemProviderWriting, NSItemProviderReading {
var text: String
init(text: String) {
self.text = text
}
static var writableTypeIdentifiersForItemProvider: [String] { [dataUTI] }
func loadData(
withTypeIdentifier typeIdentifier: String,
forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void
) -> Progress? {
let progress = Progress(totalUnitCount: 1)
DispatchQueue.global(qos: .userInitiated).async {
progress.completedUnitCount = 1
let data = self.text.data(using: .utf8)
completionHandler(data, nil)
}
return progress
}
static var readableTypeIdentifiersForItemProvider: [String] { [dataUTI] }
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> DragData {
guard let str = String(data: data, encoding: .utf8) else {
throw CocoaError(CocoaError.Code.fileReadInapplicableStringEncoding)
}
return DragData(text: str)
}
}
fileprivate func provider(for text: String) -> NSItemProvider {
NSItemProvider(object: DragData(text: text))
}
struct ContentView: View {
@State private var leftItems = (1...20).map { "Left \($0)" }
@State private var rightItems = (1...20).map { "Right \($0)" }
var body: some View {
HStack {
ItemListView(items: $leftItems)
Divider()
ItemListView(items: $rightItems)
}
}
}
struct ItemListView: View {
@Binding var items: [String]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
.itemProvider { provider(for: item) }
}
.onInsert(of: [dataUTI]) { idx, providers in
self.insert(at: idx, from: providers)
}
}
}
private func insert(at index: Int, from providers: [NSItemProvider]) {
// simple async load of all items
let group = DispatchGroup()
var strings = [String](repeating: "", count: providers.count)
DispatchQueue.concurrentPerform(iterations: providers.count) { idx in
group.enter()
providers[idx].loadObject(ofClass: DragData.self) { obj, error in
if let data = obj as? DragData {
strings[idx] = data.text
} else if let error = error {
print("Drop decode error: \(error)")
}
group.leave()
}
}
group.notify(queue: .main) {
self.items.insert(contentsOf: strings, at: index)
}
}
}
In this instance I would do away with the `data` property and just pass `store.mainlist` as the argument to your `singlePicke` view.If that weren't an option, your only way to do this would be to implement the initializer yourself, in whatever way you'd like it to work. As a `struct` type, the synthesized initializer would take values for `store`, `selectedStrength`, and `data`, each with a default value matching the one defined in the property's declaration, with the exception of 'data', which wouldn't have a default (because as you've noticed you can't reference existing properties).init(store: Userlist = Userlist(), selectedStrength: [String] = [""], data: [Mainlist]) { ... }Essentially your property initializer of `store.mainlist` is setting a default parameter value of `self.store.mainlist`, which doesn't exist. Instead, you implement this initializer yourself, or rather something functionally similar:struct ContentView: View {
@ObservedObject var store: Userlist = Userlist()
@State var selectedStrength = [""]
private var data: [Mainlist] // no initial value at declaration
init(store: Userlist = Userlist(), selectedStrength: [String] = [""]) {
self._store = ObservedObject(wrappedValue: store)
self._selectedStrength = State(wrappedValue: selectedStrength)
self.data = store.mainlist
}
...
}Note, however, that when initializing wrapped properties (i.e. @ObservedObject, @State, @Binding, etc.) you need to initialize the real value, not the 'accessor'. The actual storage for property wrapper types lives in a variable called '_name', and the 'name' property accesses '_name.wrappedValue'. In the initializer, you need to initialize the '_name' version.If you're not going to ever pass in alternatives for the store and strength, you can use a parameterless initializer, and let the compiler handle the rest:init() {
// self.store and self.selectedStrength are initialized automatically here.
self.data = self.store.mainlist
}Again though: I don't see a compelling reason to keep a whole extra copy of the same data in this view; just pass 'store.mainlist' directly.
The approach you've used works perfectly for me when I use it in the latest Xcode (11.3.1). So long as the items are Button instances they will highlight on roll-over and trigger their actions then close the menu when clicked. Here's the ContentView implementation I'm using to test this in a new macOS Application project:struct ContentView: View {
@State private var labelText = "<None>"
var body: some View {
VStack {
Text("MenuButton Trial")
MenuButton("Menu") {
Button("New Contact") {
self.labelText = "Contact"
}
Button("New Group") {
self.labelText = "Group"
}
}
.frame(maxWidth: 200)
Text("Selection: \(labelText)")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}Are you using any special button styles elsewhere in your code? Use of the .buttonStyle() modifier puts information into the environment, so it will cascade down to affect all buttons below. Attaching .buttonStyle(DefaultButtonStyle()) to your MenuButton may change this, if so.Additionally, I'll note that the menu background isn't white for me—it's a semitransparent grey. There may be something else in the environment that's affecting the menu appearance. It might be worth adding copies of your menu code to different parts of your UI to see if it appears correctly anywhere, and then that might help you track down the cause of the changed behavior.
The way to use more logic in making the background color decision is to factor it out into another method:private func backgroundColor(for addressType: AddressType) -> Color {
switch (addressType) {
case .shipFrom: return .green
case .shipTo: return .orange
case .unknown: return .yellow
}
}
...
var body: some View {
...
.background(backgroundColor(for: addressType))
}Personally, I'd be inclined to put the color choice into the AddressType itself, perhaps in an extension available to the view that needs it:extension AddressType {
var backgroundColor: Color {
switch (self) {
case .shipFrom: return .green
case .shipTo: return .orange
case .unknown: return .yellow
}
}
}
...
.background(addressType.backgroundColor)
Unfortunately there appears to be no way to do this in SwiftUI at present. Somewhat annoying, since when the library was in its pupal stages there was a very-much-public-and-settable contentOffset property on ScrollView. I can only guess that there were some very nasty edge cases that couldn't be worked out at the time.So far the only way I know of obtaining the scroll offset is to place an invisible/empty view on top of the List (i.e. in a ZStack), and another at the top of the List/ScrollView's content, and then using some anchors and preferences to obtain and publish the global coordinates of each. The distance between the second hidden view (which will move upwards as the scroll view content moves) and the first view (which stays in its location on screen) will give you the scroll offset. Unfortunately, there's nothing you can do to put that information back into the scrolling view at all, though you can use it to implement pull-to-refresh; see https://swiftui-lab.com/scrollview-pull-to-refresh/
Generally speaking, objectWillChange is called immediately before the new value is written. Further, the expectation within Combine is that the old value will only be available until the moment the objectWillChange call completes, and not afterwards; the publisher is expected to enforce this. This is one of the reasons for the existence of the @Published property wrapper, and the change from the previous guidance of just using your own 'objectWillChange' subject—the property wrapper can enforce certain behaviors, for instance locking access to its wrapped value from the point it signals the will-change event to the time the item has actually changed. For instance, a simplistic implementation might look something like this:@propertyWrapper
struct Published {
let publisher: PassthroughSubject<void, never=""> // this is a reference type
init(publisher: PassthroughSubject, wrappedValue: Value<void, never="">) {
self.publisher = publisher)
self._wrappedValue = wrappedValue
}
var lock = SomeLock()
private var _wrappedValue: Value
private var _oldValue: Value!
var wrappedValue: Value {
get {
if lock.tryLock() {
return _wrappedValue
} else {
// we're updating, and the contract is that _oldValue *must* be valid while locked.
return _oldValue
}
}
set {
_oldValue = _wrappedValue
lock.whileLocked {
publisher.send()
_wrappedValue = newValue
}
}
}
}The actual implementation will be significantly more gnarly than this, and would really need to involve some sort of multi-state condition lock (such as NSConditionLock) that would allow the getter and setter to more efficiently synchronize themselves: so the getter can read when in not-updating state and in will-change state, but not in actually-changing state, etc. (read up on condition locks and multi-threaded queue implementations to find some basic examples).
My recommendation would be to remove the call to loadData() from your controller's init() method. While convenient for single-use items, it can often lead to complex behaviors that are difficult to reason about—such as you've just found. A pattern I'd recommend for your use fould be to have init() just set up the internals, have a public loadData() method that can be called explicitly, and then have a convienience class method that does both—WarehouseOrderController.autoLoadingController() for example.Having done that, the simplest way to do what you need would be to use a `didAppear { }` block in your view that calls `warehouseOrderController.loadData()`. You'd then need to make your WarehouseOrderController conform to ObservableObject, and change its variable declaration to use the @ObservedObject attribute, so that SwiftUI can tell that you want to update when its published properties change.You'd end up with a view something like this:struct WarehouseOrderOverview: View {
@EnvironmentObject var settingsStore: SettingsStore
@ObservedObject var warehouseOrderController = WarehouseOrderController() // just init(), not loading
var body: some View {
List(warehouseOrderController.warehouseOrders) { order in
// order view here
}
.onAppear {
// load the data when first displayed.
warehouseOrderController.loadData()
}
}
}Since your warehouseOrderController is marked with @ObservableObject, when you call warehouseOrderController.warehouseOrders in your List initializer, SwiftUI will take notice and will cause your view body to be re-fetched when that property changes. Without the @ObservableObject keyword, SwiftUI doesn't know to look, and will not trigger anything when the load completes.
This is a static property, which means it's effectively a handy way to get a predefined animation. You'd use `Animation.easeIn` to get an animation which, as Claude points out, will start slowly and increase in speed.It sounds like you're under the impression that these are properties of an Animation instance though. If that's the case, I'm afraid that's not what they are. They're effectively factory methods (if you're familiar with that term) returning a new Animation instance that's configured in a certain way. We don't actually have access to any of the real properties of the Animation type. If you are interested in how to define your own animations, however, you can look at WWDC19 session 237 which discusses animations and transitions at around the 34 minute mark.
The views do indeed each have a local object, because as @DreamWorld Development pointed out you're assigning each to the result of an initializer, creating separate instances:@ObservedObject var settings = UserSettings()Each view thus owns its own UserSettings instance.In SwiftUI, your aim should be to have something owned in one place, then referenced in others. @ObservedObject is one way to do this, though you need to explicitly allocate it once and pass it to your child views. Alternatively you can use the $-syntax to pass a Binding to one of its published variables into a child view. Alternatively you can make a single ObservableObject available to an entire view tree by assigning it to the environment.In the latter case, you'd do something like this:class UserSettings: ObservableObject {
@Published var score:Int = 0
}
struct ButtonOne: View {
// Fetched from the environment on your behalf
@EnvironmentObject var settings: UserSettings
var body: some View {
HStack {
Button("Increase Store") {
self.settings.score += 1
}
Text("In ButtonOne your score is \(settings.score)")
}
}
}
struct ButtonTwo: View {
// Fetched from the environment on your behalf
@EnvironmentObject var settings: UserSettings
var body: some View {
HStack {
Button("Decrease Score") {
self.settings.score -= 1
}
Text("In ButtonTwo your score is \(settings.score)")
}
}
}
struct ContentView: View {
// Fetched from the environment on your behalf
@EnvironmentObject var settings: UserSettings
var body: some View {
VStack(spacing: 10) {
Text("In master view your score is \(settings.score)")
// Buttons inherit the environment, including the UserSettings instance.
ButtonOne()
ButtonTwo()
Text("All scores refer to the same variable, so should be the same.")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(UserSettings()) // assign environment
}
}Here the single UserSettings() instance is created by the preview provider (in the real app, this would happen in your SceneDelegate) and placed in the ContentView's environment using the .environmentObject() modifier. The ContentView and both buttons then access it by declaring a property of the appropriate type with the @EnvironmentObject attribute. SwiftUI will fetch it from the environment on demand.Now, if this is indeed a general object containing globally-useful things, then placing it within the environment is the right way to do things. If it only contains information useful within a general hierarchy, you're better off using bindings. The type of setup you have in your example lends itself to that. Here you'd declare and own your UserSettings within the ContentView, and would pass a binding to the score property into each button:class UserSettings: ObservableObject {
@Published var score:Int = 0
}
struct ButtonOne: View {
// Initialize the view with a binding to use.
@Binding var score: Int
var body: some View {
HStack {
Button("Increase Store") {
// modify the binding directly.
self.score += 1
}
Text("In ButtonOne your score is \(score)")
}
}
}
struct ButtonTwo: View {
// Initialize the view with a binding to use.
@Binding var score: Int
var body: some View {
HStack {
Button("Decrease Score") {
// modify the binding directly.
self.score -= 1
}
Text("In ButtonTwo your score is \(score)")
}
}
}
struct ContentView: View {
// ContentView owns this object.
@ObservedObject var settings = UserSettings()
var body: some View {
VStack(spacing: 10) {
Text("In master view your score is \(settings.score)")
// Buttons inherit the environment, including the UserSettings instance.
ButtonOne(score: $settings.score)
ButtonTwo(score: $settings.score)
Text("All scores refer to the same variable, so should be the same.")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView() // no special treatment here.
}
}This way you're being more explicit about what state is shared, and with whom. The buttons only use the score property, so that's all they're given.In fact, as it currently stands, the binding approach doesn't even require @ObservedObject or @Published—you can simply make UserSettings a structure type and use the @State attribute in your ContentView. This has the added effect of simplifying the memory model within your application, which is generally a Good Thing.So, for example, just change the following parts of the above sample:// This is a value type now, rather than a class.
struct UserSettings {
var score:Int = 0
}
struct ButtonOne: View {
// no changes
}
struct ButtonTwo: View {
// no changes
}
struct ContentView: View {
// ContentView owns the state data.
@State var settings = UserSettings()
var body: some View {
// no changes
}
}
struct ContentView_Previews: PreviewProvider {
// no changes
}
I hadn't realized that, but it's definitely worth noting. Both of those deal with reference types, so there may be some intrisic behavior I'm not aware of which necessitates this approach. Alternatively, for @ObservedObject at least it may be just a fairly simple setup: perhaps there's an implicit onReceive() for the publisher on the observed object that causes a re-rendering. Additionally, for environments there may be other rules as well.
Last I recall, BindableObject was renamed to ObservableObject and @ObjectBinding is now @ObservedObject. Additionally, in an ObservableObject you no longer need to implement didChange yourself, you just use the @Published attribute on any properties you want to publish and the rest will be taken care of for you.struct ContentView: View {
@ObservedObject var model = PostListViewModel()
var body: some View {
List(model.posts) { post in
Text(post.branchname)
}
}
}
final class PostListViewModel: ObservableObject {
init() {
fetchPosts()
}
@Published var posts = [Post]()
private func fetchPosts() {
Webservice().getAllPosts {
self.posts = $0
}
}
}
You can provide a handy Binding implementation that reads from & writes to the user defaults, and pass that into your Picker:struct MyView: View {
private var value: Binding<Int> =Binding(
get: { UserDefaults.standard.integer(forKey: "Value") },
set: { UserDefaults.standard.set($0, forKey: "Value") }
)
var body: some View {
Picker("Title", selection: value) {
ForEach(0..<8) { i in
Text("Item \(i)")
}
}
}
}For a more full-featured implementation using a property wrapper that can vend a binding directly:@propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
init(_ key: String, defaultValue: Value) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: Value {
get { Self.getWrapped(forKey: key, defaultValue: defaultValue) }
mutating set { Self.setWrapped(value: newValue, forKey: key) }
}
var projectedValue: Binding<Value> {
Binding(get: { Self.getWrapped(forKey: self.key, defaultValue: self.defaultValue) },
set: { Self.setWrapped(value: $0, forKey: self.key) })
}
static private func getWrapped(forKey key: String, defaultValue: Value) -> Value {
UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
}
static private func setWrapped(value: Value, forKey key: String) {
UserDefaults.standard.set(value, forKey: key)
}
}
struct MyView: View {
@UserDefault(key: "Value", defaultValue: 0) private var value
var body: some View {
Picker("Title", selection: $value) {
ForEach(0..<8) { Text("Item \($0)") }
}
}
}
This is generally happening when spinning & releasing the picker's scroller. In that case, it's still coming to rest when the counter updates, which is what causes it to reset.When your counter state variable changes, it causes your entire body to be re-evaluated. This in turn causes the picker to be re-evaluated, since it's declared in your body. The same underlying UIPickerView (or similar) is still onscreen, but when the $pickerSelection binding is queried, it returns the old value—the scroll hasn't finished, and isn't ongoing since you've lifted your finger. The picker is assigned the value from the binding and, since it's not being interacted with at this moment, it sets that value, resetting position. This is generally what you'd expect when setting the value of a UIPickerView programatically as well. Note that if you keep your finger down and moving slowly, you'll see the UI flicker back to the old value for at most a single frame, since the gesture is ongoing and that is keeping the view content updated to match. If you let go on a number a moment after the counter changes, it has time to settle and set the $pickerSelection before the counter is updated, causing another redraw.I'll note that when you use a list of static Text views inside your picker, the issue doesn't occur because the Picker doesn't know how to map the value of $pickerSelection to one of its subviews. When you use a ForEach, there's an implicit .tag() modifier attached to each Text view you create. When you just list out your own Text views, you'd need to tag each one to make it correspond to (and set) the value of $pickerSelection. If you added those modifiers, I believe you'll see the same issue as when using the ForEach.The ultimate issue is that the entire ContentView is being updated when that one property changes. This is intentional, but in your setup it's causing a re-evaluation of a VStack, one Text (the only thing referencing the changed state), one Picker, one ForEach, and eight Text items. The way to decouple the initial Text view's updates from the Picker and the rest of your hierarchy here is to separate out that first Text view and use a Binding to your counter property. This way you can retain ownership of the counter and the timer in ContentView, but only the view *using* the counter will be updated. While the ContentView's body referenced the counter property, that was the entire ContentView, but now it's just a single small subview. This makes the render tree simpler, and narrowly defines the dependencies.Here's a version of your code where the Picker behaves itself. Note that all I've done is create a new View that wraps the counter Text and uses a binding to read the counter value; the onReceive() call is still within the ContentView, and management of the counter property and timer remain there. Since only the body of FrequentlyUpdateView reads from the counter property, only that view is re-evaluated when the counter changes.struct ContentView: View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State private var counter = 0
@State private var pickerSelection = 0
var body: some View {
VStack {
FrequentlyUpdatedView(counter: $counter)
.onReceive(timer) {
self.counter += 1
print($0)
}
Picker("Snappy Picker", selection: $pickerSelection) {
ForEach(0..<8) { i in
Text("Item \(i)")
}
}
.labelsHidden()
}
}
}
struct FrequentlyUpdatedView: View {
@Binding fileprivate var counter: Int
var body: some View {
Text("Counter: \(counter)")
}
}Ultimately this is one of the reasons why using lots of small and tightly-constrained custom views is recommended in SwiftUI. There's not really any extra resource cost for doing so (unlike traditional views in UIKit or AppKit), and it means the framework can determine and perform the smallest amount of work necessary when some state changes.