In my code I am attempting to set two variables in an .onAppear() modifier. The code appears to go into an infinite loop calling the .onAppear() modifier over and over again. Eliminating either one of the two assignments has the expected outcome. Using two .onAppear() modifiers, with one statement each also works as expected (which is my work-around).
import SwiftUI
import PlaygroundSupport
struct ChartView: View {
#State private var isLocked = false
#State private var isOffset = false
let timer = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
var body: some View {
ZStack {
Text("test")
}
.onAppear(perform: {
self.isLocked = true
//self.isOffset = true
})
.onAppear {
//self.isLocked = true
self.isOffset = true
}
.onAppear {
//self.isLocked = true
//self.isLocked = true
}
.onReceive(timer, perform: { t in
print("Timer fired \(t) with lock \(self.isLocked)")
})
}
}
PlaygroundPage.current.setLiveView(ChartView())
In the code above are three .onAppear() modifiers (in my final code I would have only one), if I have zero or one statement enabled in all three blocks the code executes as expected. If I have any block with both statements enabled the code goes into an infinite loop and the timer never fires.
Using Xcode 12.3 in Playground on an iMac, as well as targeted to an iOS app running on an iPad. Also occurs in the Playground app run on an iPad.
Is this a bug, or am I doing something wrong?
I don't think its an apparent bug, but keep in mind every time #state is update, the view may update as well.
one work around is to use Bool.toggle(), instead of update the value direct.
This way reminds us with a better or careful design only when we need to.
ZStack {
Text("test")
}.onAppear {
if !self.isLocked { self.isLocked.toggle()}
if !self.isOffset { self.isOffset.toggle()}
}.onReceive(timer, perform: { t in
print("Timer fired \(t) with lock \(self.isLocked)")
})
Related
I want to have a Bool property, that represents that option key is pressed #Publised var isOptionPressed = false. I would use it for changing SwiftUI View.
For that, I think, that I should use Combine to observe for key pressure.
I tried to find an NSNotification for that event, but it seems to me that there are no any NSNotification, that could be useful to me.
Since you are working through SwiftUI, I would recommend taking things just a step beyond watching a Publisher and put the state of the modifier flags in the SwiftUI Environment. It is my opinion that it will fit in nicely with SwiftUI's declarative syntax.
I had another implementation of this, but took the solution you found and adapted it.
import Cocoa
import SwiftUI
import Combine
struct KeyModifierFlags: EnvironmentKey {
static let defaultValue = NSEvent.ModifierFlags([])
}
extension EnvironmentValues {
var keyModifierFlags: NSEvent.ModifierFlags {
get { self[KeyModifierFlags.self] }
set { self[KeyModifierFlags.self] = newValue }
}
}
struct ModifierFlagEnvironment<Content>: View where Content:View {
#StateObject var flagState = ModifierFlags()
let content: Content;
init(#ViewBuilder content: () -> Content) {
self.content = content();
}
var body: some View {
content
.environment(\.keyModifierFlags, flagState.modifierFlags)
}
}
final class ModifierFlags: ObservableObject {
#Published var modifierFlags = NSEvent.ModifierFlags([])
init() {
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
self?.modifierFlags = event.modifierFlags
return event;
}
}
}
Note that my event closure is returning the event passed in. If you return nil you will prevent the event from going farther and someone else in the system may want to see it.
The struct KeyModifierFlags sets up a new item to be added to the view Environment. The extension to EnvironmentValues lets us store and
retrieve the current flags from the environment.
Finally there is the ModifierFlagEnvironment view. It has no content of its own - that is passed to the initializer in an #ViewBuilder function. What it does do is provide the StateObject that contains the state monitor, and it passes it's current value for the modifier flags into the Environment of the content.
To use the ModifierFlagEnvironment you wrap a top-level view in your hierarchy with it. In a simple Cocoa app built from the default Xcode template, I changed the application SwiftUI content to be:
struct KeyWatcherApp: App {
var body: some Scene {
WindowGroup {
ModifierFlagEnvironment {
ContentView()
}
}
}
}
So all of the views in the application could watch the flags.
Then to make use of it you could do:
struct ContentView: View {
#Environment(\.keyModifierFlags) var modifierFlags: NSEvent.ModifierFlags
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
if(modifierFlags.contains(.option)) {
Text("Option is pressed")
} else {
Text("Option is up")
}
}
.padding()
}
}
Here the content view watches the environment for the flags and the view makes decisions on what to show using the current modifiers.
Ok, I found easy solution for my problem:
class KeyPressedController: ObservableObject {
#Published var isOptionPressed = false
init() {
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event -> NSEvent? in
if event.modifierFlags.contains(.option) {
self?.isOptionPressed = true
} else {
self?.isOptionPressed = false
}
return nil
}
}
}
I'm building a new version of an app in SwiftUI and have run into a conundrum.
The scenario is that I have a canvas with multiple objects on it, and I would like to have an inspector view that shows details of the object currently under the pointer. The problem is that my initial implementation makes the whole canvas redraw far too many times.
What I have is a #State variable which the inspector displays, so the inspector view needs to see this state. The object view needs to be able to write to the state variable, so that means that when the pointer comes over an object, it changes the state variable, which then means that both the object (really, the canvas) and the inspector get marked as needing to be redrawn.
What would be nice would be for the object view to be able to write to the state that the inspector needs without invalidating its own view, but I can't work out how to do this in SwiftUI. In the old paradigm, I'd send a message to the inspector, but that doesn't seem to be the way in the declarative paradigm of SwiftUI. I suspect that some sort of observable object may be a solution, but I haven't worked out how to do that.
Related is the behaviour of .onHover, which I am using to generate the mouse entered/mouse exited events like so:
.onHover { entered in
inspectorText = entered ? self.descriptionText : ""
}
I seem to get multiple enter/exit events, presumably due to the redrawing, which slows things to a crawl. Is there a better method than using .onHover?
I'm pretty sure that there is a clever way (maybe with Equatable views...?) for preventing re-evaluation of the main body, but for what is worth here is a possible solution using a singleton:
import SwiftUI
class HoverState: ObservableObject {
static let shared: HoverState = HoverState()
#Published var currentItemText: String = ""
}
struct InspectorView: View {
#ObservedObject var hoverState: HoverState = HoverState.shared
var body: some View {
Text(hoverState.currentItemText)
}
}
struct ContentView: View {
var body: some View {
HStack {
VStack {
Color.red
.onHover { _ in HoverState.shared.currentItemText = "Red" }
Color.green
.onHover { _ in HoverState.shared.currentItemText = "Green" }
Color.blue
.onHover { _ in HoverState.shared.currentItemText = "Blue" }
}
.padding()
InspectorView()
.frame(width: 100)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This works because only the inspector view declares a dependency to the hoverState, which also means of course that if you try to reflect a value (as opposed to just setting the value) in the ContentView will not be updated...
I have a List in SwiftUI App on MacOS with e.g. 10.000 entries.
Trying like the example below is horribly slow.
Adding .id(UUID()) to the List, which was advised in a prior post, makes it a bit quicker but still not fluid.
Even worst, adding .id(UUID()) to the list, the list then cannot be navigates by the arrow-key (up/down).
Is there a better way to achieve this?
struct TestViews_MacOS_BigList: View {
#State var selectedItem: String?
var items: [String]
var body: some View {
List(items,id: \.self, selection: $selectedItem ) { item in
Text("\(item)").tag("\(item)")
}
//.id(UUID())
}
}
func testnames()->[String]{
var list: [String] = []
for i in 1...10000 {
list.append("Sting Nr \(i)")
}
return list
}
Those are too many Views to have sitting around. You need to use CoreData or some other manual way to Batch load items and a way to only have a certain number of Views/items fetched/loaded at a time.
An NSFetchedResultsController that specifies a batch size can help with that
let fetchRequest: NSFetchRequest<Item> = NSFetchRequest<Item>(entityName: "Item")
fetchRequest.includesPendingChanges = false
fetchRequest.fetchBatchSize = 20
When the fetch is executed, the entire request is evaluated and the identities of all matching objects recorded, but only data for objects up to the batchSize will be fetched from the persistent store at a time.
#FetchRequest might do it as well, it was made for SwiftUI so it should compensate but the documentation does not specify.
Try using LazyVStack since it uses memory efficiently as below:
struct TestViews_MacOS_BigList: View {
#State var selectedItem: String?
var items: [String]
var body: some View {
ScrollView {
LazyVStack{
ForEach(items, id: \.self, content: { item in
HStack{
Button(action: {
selectedItem = item
}, label: {
Text("Select ")
})
Text("\(item)").tag("\(item)")
}
})
}
}
}
}
I've come to SwiftUI from UIKit and I'm having trouble with a NavigationLink not animating when presenting a new View.
I've set up the view structure to include the NavigationLink when the following property is non-nil:
#State private var retrievedDeviceIdentity: Proteus.DeviceIdentity?
The Proteus.DeviceIdentity type is a basic data struct. This property is populated by a successful asynchronous closure, rather than a direct user interaction. Hence, the view structure is set up like so, using NavigationLink's destination:isActive:label: initialiser:
var body: some View {
NavigationView {
VStack {
Form {
// Form building
}
if let deviceIdentity = retrievedDeviceIdentity {
NavigationLink(
destination: AddDeviceLinkDeviceForm(deviceIdentity: deviceIdentity),
isActive: .constant(retrievedDeviceIdentity != nil),
label: {
EmptyView()
}
)
.onDisappear() {
updateSyncButtonEnabledState()
}
}
}
}
}
When retrievedDeviceIdentity is populated to be non-nil the new View is indeed presented. However, there is no slide transition to that View; it just changes immediately. When in that new view, tapping on the back button does do the slide transition back to this view.
Any ideas how to fix this? As I'm pretty new to SwiftUI if I've set the new structure up wrong then I'd welcome feedback on that too.
(I'm using Xcode 12.3 on macOS Big Sur 11.0.1.)
#Asperi got close, but moving the NavigationLink led to the view not presenting at all.
What did work was removing the if brace unwrapping retrievedDeviceIdentity:
var body: some View {
NavigationView {
VStack {
Form {
// Form building
}
NavigationLink(
destination: AddDeviceLinkDeviceForm(deviceIdentity: deviceIdentity),
isActive: .constant(retrievedDeviceIdentity != nil),
label: {
EmptyView()
}
)
.onDisappear() {
updateSyncButtonEnabledState()
}
}
}
This required AddDeviceLinkDeviceForm's deviceIdentity property to be made optional to accept the wrapped value.
I think it is due to conditional injection, try instead to have it persistently included in view hierarchy (and so be registered in NavigationView), like
VStack {
Form {
// Form building
}
}
.background(
NavigationLink(
destination: AddDeviceLinkDeviceForm(deviceIdentity: retrievedDeviceIdentity),
isActive: .constant(retrievedDeviceIdentity != nil),
label: {
EmptyView()
}
)
.onDisappear() {
updateSyncButtonEnabledState()
}
)
Note: I'm not sure about your expectation for .onDisappear and why do you need it, probably it will be needed to move in some other place or under different modifier.
I'm building an app that shares quite a bit of SwiftUI code between its iOS and macOS targets. On iOS, onDisappear seems to work reliably on Views. However, on macOS, onDisappear doesn't get called if the View is inside a sheet or popover.
The following code illustrates the concept:
import SwiftUI
struct ContentView: View {
#State private var textShown = true
#State private var showSheet = false
#State private var showPopover = false
var body: some View {
VStack {
Button("Toggle text") {
self.textShown.toggle()
}
if textShown {
Text("Text").onDisappear {
print("Text disappearing")
}
}
Button("Toggle sheet") {
self.showSheet.toggle()
}.sheet(isPresented: $showSheet, onDismiss: {
print("On dismiss")
}) {
VStack {
Button("Close sheet") {
self.showSheet = false
}
}.onDisappear {
print("Sheet disappearing")
}
}
Button("Toggle popover") {
self.showPopover.toggle()
}.popover(isPresented: $showPopover) {
VStack {
Text("popover")
}.onDisappear {
print("Popover disappearing")
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Note that onDisappear works fine on the Text component at the beginning of the VStack but the other two onDisappear calls don't get executed on macOS.
One workaround I've found is to attach an ObservableObject to the View and use deinit to call cleanup code. However, this isn't a great solution for two reasons:
1) With the popover example, there's a significant delay between the dismissal of the popover and the deist call (although it works quickly on sheets)
2) I haven't had any crashes on macOS with this approach, but on iOS, deinit have been unreliable in SwiftUI doing anything but trivial code -- holding references to my data store, app state, etc. have had crashes.
Here's the basic approach I used for the deinit strategy:
class DeinitObject : ObservableObject {
deinit {
print("Deinit obj")
}
}
struct ViewWithObservableObject : View {
#ObservedObject private var deinitObj = DeinitObject()
var body: some View {
Text("Deinit view")
}
}
Also, I would have thought I could use the onDismiss parameter of the sheet call, but that doesn't get called either on macOS. And, it's not an available parameter of popover.
All of this is using Xcode 11.4.1 and macOS 10.15.3.
Any solutions for good workarounds?