TextField in sheet that accepts focus immediately without any noticeable delay - swiftui

I want to open a Sheet in SwiftUI that 1) contains a TextField which 2) accepts focus immediately. Unfortunately, as demonstrated in the attached clip, there’s a short but noticeable delay before the keyboard pops up. This makes the transition into the focused state not as smooth as I would like (UI shifting to make room for the keyboard).
Minimum reproducible example:
struct TestRootView: View {
#State private var sheet = false
var body: some View {
Button("Open sheet") {
sheet.toggle()
}
.sheet(isPresented: $sheet) {
SheetView()
}
}
}
struct SheetView: View {
#Environment(\.dismiss) var dismiss
#FocusState var isFocused
#State var text: String = ""
var body: some View {
TextField("title", text: $text)
.focused($isFocused)
.onAppear {
isFocused = true
}
Button("Close") {
dismiss()
}.font(.title)
.padding()
.background(.black)
}
}

Related

Swift UI #FocusState triggering NavigationLink Twice

What am I doing wrong here? Tapping on the navigation link triggers the navigation, then the focusState value updates, causing body to run and trigger the navigation link again.
How do I prevent the link from being triggered twice causing my destination views init to fire twice?
import SwiftUI
struct ContentView: View {
#State var text: String = "text"
#FocusState var focussed: Bool
#State var isActive: Bool = false
var body: some View {
NavigationView {
List {
TextField("", text: $text)
.focused($focussed)
.onChange(of: focussed) { _ in }
let _ = Self._printChanges()
NavigationLink("Tap Me", destination: MyViewTwo(), isActive: $isActive)
}
}
}
}
struct MyViewTwo: View {
init() {
print("Init Called")
}
var body: some View {
Text("Hello View 2")
}
}

SwiftUI publishing an environment change from within view update

The app has a model that stores the user's current preference for light/dark mode, which the user can change by clicking on a button:
class DataModel: ObservableObject {
#Published var mode: ColorScheme = .light
The ContentView's body tracks the model, and adjusts the colorScheme when the model changes:
struct ContentView: View {
#StateObject private var dataModel = DataModel()
var body: some View {
NavigationStack(path: $path) { ...
}
.environmentObject(dataModel)
.environment(\.colorScheme, dataModel.mode)
As of Xcode Version 14.0 beta 5, this is producing a purple warning: Publishing changes from within view updates is not allowed, this will cause undefined behavior. Is there another way to do this? Or is it a hiccup in the beta release? Thanks!
Update: 2022-09-28
Xcode 14.1 Beta 3 (finally) fixed the "Publishing changes from within view updates is not allowed, this will cause undefined behavior"
See: https://www.donnywals.com/xcode-14-publishing-changes-from-within-view-updates-is-not-allowed-this-will-cause-undefined-behavior/
Full disclosure - I'm not entirely sure why this is happening but these have been the two solutions I have found that seem to work.
Example Code
// -- main view
#main
struct MyApp: App {
#StateObject private var vm = ViewModel()
var body: some Scene {
WindowGroup {
ViewOne()
.environmentObject(vm)
}
}
}
// -- initial view
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
// -- sheet view
struct SheetView: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Close sheet")
}
}
}
// -- view model
class ViewModel: ObservableObject {
#Published var isPresented: Bool = false
}
Solution 1
Note: from my testing and the example below I still get the error to appear. But if I have a more complex/nested app then the error disappears..
Adding a .buttonStyle() to the button that does the initial toggling.
So within the ContentView on the Button() {} add in a .buttonStyle(.plain) and it will remove the purple error:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.buttonStyle(.plain) // <-- here
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
^ This is probably more of a hack than solution since it'll output a new view from the modifier and that is probably what is causing it to not output the error on larger views.
Solution 2
This one is credit to Alex Nagy (aka. Rebeloper)
As Alex explains:
.. with SwiftUI 3 and SwiftUI 4 the data handling kind of changed. How SwiftUI handles, more specifically the #Published variable ..
So the solution is to have the boolean trigger to be a #State variable within the view and not as a #Published one inside the ViewModel. But as Alex points out it can make your views messy and if you have a lot of states in it, or not be able to deep link, etc.
However, since this is the way that SwiftUI 4 wants these to operate, we run the code as such:
// -- main view
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ViewOne()
}
}
}
// -- initial view
struct ViewOne: View {
#State private var isPresented = false
var body: some View {
Button {
isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
// SheetView() <-- if using dismiss() in >= iOS 15
}
}
}
// -- sheet view
struct SheetView: View {
// I'm showing a #Binding here for < iOS 15
// but you can use the dismiss() option if you
// target higher
// #Environment(\.dismiss) private var dismiss
#Binding var isPresented: Bool
var body: some View {
Button {
isPresented.toggle()
// dismiss()
} label: {
Text("Close sheet")
}
}
}
Using the #Published and the #State
Continuing from the video, if you need to still use the #Published variable as it might tie into other areas of your app you can do so with a .onChange and a .onReceive to link the two variables:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
#State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.onReceive(vm.$isPresented) { newValue in
isPresented = newValue
}
.onChange(of: isPresented) { newValue in
vm.isPresented = newValue
}
}
}
However, this can become really messy in your code if you have to trigger it for every sheet or fullScreenCover.
Creating a ViewModifier
So to make it easier for you to implement it you can create a ViewModifier which Alex has shown works too:
extension View {
func sync(_ published: Binding<Bool>, with binding: Binding<Bool>) -> some View {
self
.onChange(of: published.wrappedValue) { newValue in
binding.wrappedValue = newValue
}
.onChange(of: binding.wrappedValue) { newValue in
published.wrappedValue = newValue
}
}
}
And in use on the View:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
#State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.sync($vm.isPresented, with: $isPresented)
// .onReceive(vm.$isPresented) { newValue in
// isPresented = newValue
// }
// .onChange(of: isPresented) { newValue in
// vm.isPresented = newValue
// }
}
}
^ Anything denoted with this is my assumptions and not real technical understanding - I am not a technical knowledgeable :/
Try running the code that's throwing the purple error asynchronously, for example, by using DispatchQueue.main.async or Task.
DispatchQueue.main.async {
// environment changing code comes here
}
Task {
// environment changing code comes here
}
Improved Solution of Rebel Developer
as a generic function.
Rebeloper solution
It helped me a lot.
1- Create extension for it:
extension View{
func sync<T:Equatable>(_ published:Binding<T>, with binding:Binding<T>)-> some View{
self
.onChange(of: published.wrappedValue) { published in
binding.wrappedValue = published
}
.onChange(of: binding.wrappedValue) { binding in
published.wrappedValue = binding
}
}
}
2- sync() ViewModel #Published var to local #State var
struct ContentView: View {
#EnvironmentObject var viewModel:ViewModel
#State var fullScreenType:FullScreenType?
var body: some View {
//..
}
.sync($viewModel.fullScreenType, with: $fullScreenType)

How dismiss keyboard in a sheet with inherited toolbar+button

A view has a TextField and the keyboard has a "Dismiss Keyboard" button (in the toolbar) to remove focus from the textField and dismiss the keyboard. That works fine but when a sheet appears that also contains a TextField the keyboard inherits the button from the previous view and will not dismiss the sheet's keyboard.
I have tried overwriting with a new ToolbarItemGroup within the sheet but it has no effect. The button references the first view. Is there a way to have each keyboard appearance reference the view within which the relevant TextField is situated?
import SwiftUI
struct ContentView: View {
#FocusState private var focusedField1: Bool
#State private var name1 = ""
#State private var showSheet = false
var body: some View {
VStack {
TextField("Hello", text: $name1)
.focused($focusedField1)
.textFieldStyle(.roundedBorder)
.padding()
Button("Show Sheet") { showSheet = true }
}
.toolbar {
toolBarContent1()
}
.sheet(isPresented: $showSheet) {
MySheet()
}
}
#ToolbarContentBuilder func toolBarContent1() -> some ToolbarContent {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button {
print("11111")
focusedField1 = false
} label: { Text("Dismiss Keyboard") }
}
}
}
struct MySheet: View {
#Environment(\.dismiss) var dismiss
#FocusState private var focusedField2: Bool
#State private var name2 = ""
var body: some View {
VStack {
TextField("Hello", text: $name2)
.focused($focusedField2)
.textFieldStyle(.roundedBorder)
.padding()
Button("Dismiss Sheet") {
focusedField2 = false
dismiss()
}
.toolbar {
toolBarContent2()
}
}
}
#ToolbarContentBuilder func toolBarContent2() -> some ToolbarContent {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button {
print("22222")
focusedField2 = false
} label: { Text("Dismiss Keyboard") }
}
}
}
Your code is fine. To make this work, just put your sheet inside a NavigationView{}. Code is below the image:
.sheet(isPresented: $showSheet) {
NavigationView {
MySheet()
}
}

SwiftUI sheet infinite loop?

Is this a bug in SwiftUI? If you tap "Test" it goes into a loop, putting up sheet after sheet forever. I can't see why.
This is happening on iOS and iPadOS 15
struct ContentView: View {
#State private var showSheet = false
#State private var name: String = ""
#FocusState private var isFocused: Bool
var body: some View {
VStack {
Button("Test") { setState() }
}
.sheet(isPresented: $showSheet) {
VStack {
Text("\(showSheet.description), \(name)")
TextField("folder name", text: $name)
focused($isFocused)
}
}
}
private func setState() {
print("setState")
showSheet = true
}
}
This is a typo, and normally should be just closed as such, but I think the reason is interesting enough to warrant an answer here:
struct ContentView: View {
#State private var showSheet = false
#State private var name: String = ""
#FocusState private var isFocused: Bool
var body: some View {
VStack {
Button("Test") { setState() }
}
.sheet(isPresented: $showSheet) {
VStack {
Text("\(showSheet.description), \(name)")
TextField("folder name", text: $name)
focused($isFocused) //<-- THIS LINE IS MISSING A `.` -- it should be .focused($isFocused)
}
}
}
private func setState() {
print("setState")
showSheet = true
}
}
Because you've omitted a . on the focused line, focused is returning a modifier on ContentView itself rather than on the TextField. That means that it's adding a copy of ContentView to the view hierarchy below TextField (since focused returns a modified version of self) rather than modifying the TextField in place.
This is causing the loop, but because technically it's valid code, it doesn't generated a compiler error.

SwiftUI : how to make this raising up view

I am just curious for this animation, when i click one button, the origin page will fall down a bit and the new page will raise up with a cross button on top left corner.
Seems it's quite a common behavior, anyone who know is there any official api for this animation ?
This is just two sheets presented, one on top of the other.
Example code:
struct ContentView: View {
#State private var isPresented = false
var body: some View {
VStack {
Text("Content")
Button("Present sheet") {
isPresented = true
}
.sheet(isPresented: $isPresented) {
MainSheet()
}
}
}
}
struct MainSheet: View {
#State private var isPresented = false
var body: some View {
VStack {
Text("Sheet content")
Button("Present another sheet") {
isPresented = true
}
.sheet(isPresented: $isPresented) {
Text("Final content")
}
}
}
}
Result: