I have a SwiftUI button that, when clicked, a sheet displays a confirmation modal. When a button is clicked in that modal to confirm, I make a save to Core Data.
I'm getting one or all of a few nasty results:
The sheet just hangs and becomes unresponsive.
I get a warning: "Modifying state during view update, this will cause undefined behavior".
I get a crash.
Obviously I'm modifying the state becomes I'm deleting a thing, but I'm unclear on how to do this right.
Here is my view where I click delete:
struct MyView: View{
#State var showDeleteModal = false
var body: some View{
Button("Delete"){
showDeleteModal.toggle()
}
.sheet(isPresented: self.$showDeleteModal) {
ModalView(confirm: {
self.showDeleteModal.toggle()
//Save the object in my Core Data stuff
model.saveThing(thing: thing)
})
}
}
}
And here's my modal that has a callback function to confirm the deletion:
struct ModalView: View {
var confirm:() -> Void
var body: some View {
Button("Confirm"){
confirm()
}
}
}
How do I hide the modal and make the save (which removes the view from the screen) without interfering with SwiftUI's state?
I'd suggest using sheet(isPresented:onDismiss:content:) and calling model.saveThing in onDismiss:
struct MyView: View {
#State var showDeleteModal = false
var body: some View {
Button("Delete") {
showDeleteModal.toggle()
}
.sheet(isPresented: self.$showDeleteModal, onDismiss: onDismiss) {
ModalView()
}
}
func onDismiss() {
model.saveThing(thing: thing)
}
}
Then you can dismiss the sheet without knowing the parent's state - just use #Environment(\.presentationMode):
struct ModalView: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
Button("Confirm") {
presentationMode.wrappedValue.dismiss()
}
}
}
Note: as sheet can be closed without interacting with the confirm button, you can detect how it was closed using another #State variable - see:
SwiftUI: How to show an alert after a sheet is closed?
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 have two TextFields that use onCommit. Pressing enter saves the value. However, this does not automatically move the cursor to the next TextField. I want it to work like the tab button which moves the cursor to next TextField but this doesn't save the value (due to onCommit requiring enter/return to be pressed). The best solution I have found is using a button but that results in poor usability as I would be using a ForLoop over this view.
struct ModuleDetailView: View {
#Binding var subjectDetails: [Subjects]
#State private var subject = ""
#State private var grade = ""
var body: some View {
VStack {
TextField("Subject", text: $subject, onCommit: appendData)
TextField("Grade", text: $grade, onCommit: appendData)
VStack {
Text("Output")
ForEach(subjectDetails) { subject in
HStack {
Text(subject.name)
Text(subject.grade)
}
}
}
}
}
func appendData() {
if subject != "" && grade != "" {
let module = Subjects(name: subject, grade: grade)
subjectDetails.append(module)
}
}
}
The preview code:
struct ModuleDetailView_Previews: PreviewProvider {
static var previews: some View {
PreviewWrapper()
}
struct PreviewWrapper: View {
#State var modules = [Subjects]()
var body: some View {
ModuleDetailView(subjectDetails: $modules)
}
}
}
both textfields are visible at the same time. What happens is that after I press enter from the first textField the cursor just vanishes, which works differently from when we press TAB - simply moves to the next. And I want it to work similar to how it behaves when TAB is pressed. Therefore, in this case using a firstResponder might not be a good option
Subjects struct:
struct Subjects: Identifiable {
let id = UUID()
var name: String
var grade: String
}
I am currently developing an app for watchOS 6 (independent app) using Swift/SwiftUI in XCode 11.5 on macOS Catalina.
Before a user can use my app, a configuration process is required. As the configuration process consists of several different views which are shown one after each other, I implemented this by using navigation links.
After the configuration process has been finished, the user should click on a button to return to the "main" app (main view). For controlling views which are on the same hierarchical level, my plan was to use an EnvironmentObject (as far as I understood, an EnvironmentObject once injected is handed over to the subviews and subviews can use the EnvironmentObject) in combination with a "controlling view" which controls the display of the views. Therefore, I followed the tutorial: https://blckbirds.com/post/how-to-navigate-between-views-in-swiftui-by-using-an-environmentobject/
This is my code:
ContentView.swift
struct ContentView: View {
var body: some View {
ContentViewManager().environmentObject(AppStateControl())
}
}
struct ContentViewManager: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
if(appStateControl.callView == "AppConfig") {
AppConfig()
}
if(appStateControl.callView == "AppMain") {
AppMain()
}
}
}
}
AppStateControl.swift
class AppStateControl: ObservableObject {
#Published var callView: String = "AppConfig"
}
AppConfig.swift
struct AppConfig: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Main")
NavigationLink(destination: DetailView1().environmentObject(appStateControl)) {
Text("Show Detail View 1")
}
}
}
}
struct DetailView1: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Detail View 1")
NavigationLink(destination: DetailView2().environmentObject(appStateControl)) {
Text("Show Detail View 2")
}
}
}
}
struct DetailView2: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Detail View 2")
Button(action: {
self.appStateControl.callView = "AppMain"
}) {
Text("Go to main App")
}
}
}
}
AppMain.swift
struct AppMain: View {
var body: some View {
Text("Main App")
}
}
In a previous version of my code (without the handing over of the EnvironmentObject all the time) I got a runtime error ("Thread 1: Fatal error: No ObservableObject of type AppStateControl found. A View.environmentObject(_:) for AppStateControl may be missing as an ancestor of this view.") caused by line 41 in AppConfig.swift. In the internet, I read that this is probably a bug of NavigationLink (see: https://www.hackingwithswift.com/forums/swiftui/environment-object-not-being-inherited-by-child-sometimes-and-app-crashes/269, https://twitter.com/twostraws/status/1146315336578469888). Thus, the recommendation was to explicitly pass the EnvironmentObject to the destination of the NavigationLink (above implementation). Unfortunately, this also does not work and instead a click on the button "Go to main App" in "DetailView2" leads to the view "DetailView1" instead of "AppMain".
Any ideas how to solve this problem? To me, it seems that a change of the EnvironmentObject in a view called via a navigation link does not refresh the views (correctly).
Thanks in advance.
One of the solutions is to create a variable controlling whether to display a navigation stack.
class AppStateControl: ObservableObject {
...
#Published var isDetailActive = false // <- add this
}
Then you can use this variable to control the first NavigationLink by setting isActive parameter. Also you need to add .isDetailLink(false) to all subsequent links.
First link in stack:
NavigationLink(destination: DetailView1().environmentObject(appStateControl), isActive: self.$appStateControl.isDetailActive) {
Text("Show Detail View 1")
}
.isDetailLink(false)
All other links:
NavigationLink(destination: DetailView2().environmentObject(appStateControl)) {
Text("Show Detail View 2")
}
.isDetailLink(false)
Then just set isDetailActive to false to pop all your NavigationLinks and return to the main view:
Button(action: {
self.appStateControl.callView = "AppMain"
self.appStateControl.isDetailActive = false // <- add this
}) {
Text("Go to main App")
}
Testing some stuff on tvOS with SwiftUI. When i add a custom style to a button, the "action" of the button is not getting triggered. In this example, i wanna print "pressed", but adding functions is not working either.
Is it possible to implement an custom action trigger aswell or what am i doing wrong?
For clarification why i wanna have a custom button style.
When i dont use any buttonstyle, the "action" is working, but the "onFocusChange" function is never getting called. WHICH I NEED!
But.. when i use a buttonstyle, the onFocusChange is working but the action is not.....
struct CustomButton: View {
#State private var buttonFocus: Bool = false
var body: some View {
VStack(alignment: .center){
Button(action: {
print("pressed")
})
{
Text("Save")
}
.buttonStyle(TestButtonStyle(focused: buttonFocus))
.focusable(true, onFocusChange: { (changed) in
self.buttonFocus.toggle()
})
}
}
}
Buttonstyle:
struct TestButtonStyle: ButtonStyle {
let focused: Bool
public func makeBody(configuration: TestButtonStyle.Configuration) -> some View {
configuration.label
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 5).fill(Color.red))
.scaleEffect(focused ? 1.5 : 1.0)
}
}
Its working with SwiftUI 2.0 now.
You can use the following environment Object on your View you want to be focused.
Also, add a State or Binding to bubble up to your parent view.
#Environment(\.isFocused) var isFocused: Bool
#Binding var focusedValue: Bool
Then, you can call the following modifier, which gets called when the View is getting focused or not. Here you change your Binding or State variable.
.onChange(of: isFocused, perform: { value in
self.focusedValue = value
})
Finally, you can use your Binding or State to modify your View.
.scaleEffect(self.focused ? 1.1 : 1)
.animation(.linear(duration: 0.1))
I am trying to make a popover in SwiftUI using a UIHostingController with a list that can be tapped. First, the user name and password should be filled in, and then the user role should be tapped in the list, and the popover should be dismissed when the save button is tapped.
Also, the save button in the navigation bar should be disabled until the user information has been verified.
The Xcode playground for this can be fetched from my GitHub repository https://github.com/imyrvold/Popover
To be able to use the AddUserView as a rootView in UIHostingController, I had to use an Xcode storyboard, and add it to the Resources in the Xcode Playground.
import SwiftUI
import Combine
public struct AddUserView : View {
#ObjectBinding public var loginInfo: LoginInfo
#EnvironmentObject var viewModel: RoleViewModel
#State var selectedRole: Role? = nil
#Environment(\.isPresented) var isPresented: Binding<Bool>?
public var body: some View {
NavigationView {
VStack {
TextField(self.$loginInfo.firstName, placeholder: Text("First Name"))
TextField(self.$loginInfo.lastName, placeholder: Text("Last Name"))
TextField(self.$loginInfo.email, placeholder: Text("Email"))
SecureField(self.$loginInfo.password, placeholder: Text("Password"))
Divider()
List(self.viewModel.roles) { role in
RoleCell(role: role).tapAction {
self.selectedRole = role
}
}
}
.padding()
.navigationBarTitle(Text("Add User"))
.navigationBarItems(trailing:
Button(action: {
self.saveAction()
self.isPresented?.value = false
}) {
Text("Save")
})//.disabled(!self.loginInfo.isValid)
}
}
// MARK:- Action methods
func saveAction() {
}
}
The first problem I have is that when I uncomment the disabled(!self.loginInfo.isValid), all the TextField's are also disabled. Not sure if that is a bug in SwiftUI?
I also want to have the rolecell set the checkmark on the cell when tapped, but so far I have been unable to figure out how to do that.
And how can I dismiss the Popover when the save button is tapped?
(When running the playground, have to click the start playground a second time to run properly, the first time the Save popover doesn't work).
Have you tried this
.navigationBarItems(trailing:
Button(action: {
self.saveAction()
self.isPresented?.value = false
}) {
Text("Save")
}.disabled(!self.loginInfo.isValid))