How can I use the pressed key information in my SwiftUI?
I try to use #EnvironmentObject to share key with my SwiftUI but i get every time crash in sendKeybKey: Thread 1: Fatal error: No ObservableObject of type UserData found. A View.environmentObject(_:) for UserData may be missing as an ancestor of this view.
In .environmentObject is already used in SceneDelegate, but was is wrong?
My Code:
class KeyTestController<Content>: UIHostingController<Content> where Content: View {
#EnvironmentObject var userData: UserData
override func becomeFirstResponder() -> Bool {
true
}
override var keyCommands: [UIKeyCommand]? {
var keys = [UIKeyCommand]()
for num in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!\r" {
keys.append(
UIKeyCommand(
input:String(num),
modifierFlags: [],
action:
#selector(sendKeybKey)
)
)
}
return keys
}
#objc func sendKeybKey(_ sender: UIKeyCommand) {
userData.collectChar = sender.input ?? ""
print(">>> test was pressed \(sender.input ?? "")")
}
}
In SceneDelegate
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = KeyTestController/*UIHostingController*/(
rootView: myList()
.environmentObject(UserData())
)
self.window = window
window.makeKeyAndVisible()
}
and UserData:
final class UserData: ObservableObject {
#Published var datasetUIIdata: [DatasetUII] = []
#Published var collectChar : String = ""
#Published var collectText : String = ""
}
My SwiftUI View
struct myList: View {
#EnvironmentObject var userData: UserData
var body: some View {
VStack {
HStack {
Text("Key\(userData.collectChar)").font(.largeTitle)
}
.padding()
List {
ForEach(userData.datasetUIIdata, id: \.self) { (item) in
Text(item.userName)
}
.onDelete(perform: delete)
}
.padding()
}
}
func delete(at offsets: IndexSet) {
userData.datasetUIIdata.remove(atOffsets: offsets)
}
}
Anyone up for a quick help to a newbie?
Thanks!
Waldemar
You don't need #EnvironmentObject in controller, UserData is a reference type, so you can inject same object directly in view and in controller.
class KeyTestController<Content>: UIHostingController<Content> where Content: View {
var userData: UserData
so creation then will be as below
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let userData = UserData()
let contentView = myList().environmentObject(userData) // in view !!
let controller = KeyTestController(rootView: contentView)
controller.userData = userData // in controller !!
window.rootViewController = controller
Related
I am attempting to capture a screenshot of my view in SwiftUI. I have tried with both the ImageRenderer(content: myview) and with the below snapshot View extension. On both cases it crashes giving the error . . .
Fatal error: No ObservableObject of type isActive found. A
View.environmentObject(_:) for isActive may be missing as
an ancestor of this view.
I have tried both an empty environment object and the object with variables and it always get the same error. Is there any way to allow the use of environment objects when programmatically capturing a screenshot?
//main view
struct TestView111: View {
var body: some View {
VStack{
otherview
//click this to capture screenshot and break on environment var
Button(action: {
let image = otherview.snapshot()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}, label: {
Text("Save").buttonStyleBlue()
})
}
}
//view to take snapshot of
var otherview: some View {
TestView112()
}
}
//sub view
struct TestView112: View {
#EnvironmentObject private var objRect: GraphObjectRectList
var body: some View {
//can no longer find isActive here and breaks on btn click
ForEach(objRect.isActive.indices, id: \.self) { i in
Text(String(i))
}
}
}
//extension to take snapshot
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let size = CGSize(width: 500, height: 500)
view?.bounds = CGRect(origin: .zero, size: size)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
//my Environment class
class GraphObjectRectList: ObservableObject {
#Published var isActive: [Bool] = [true, true]
}
//Scene View
struct TestAppApp: App {
var body: some Scene {
WindowGroup {
TestView111()
.environmentObject(GraphObjectRectList())
}
}
}
Is the graphics rendering engine is unable to access the global object twice?
Thanks for any help!
In your current example, the EnvironmentObject doesn't exist on otherview because otherview doesn't exist in the view hierarchy -- it exists on its own in the Button's action.
To solve this, inject it on the version you're sending to snapshot:
struct TestView111: View {
#EnvironmentObject private var objRect: GraphObjectRectList //<-- Here
var body: some View {
VStack{
otherview //<-- This one has a reference to the object, since it's in the view hierarchy
Button(action: {
let image = otherview.environmentObject(objRect).snapshot() //<-- Here
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}, label: {
Text("Save")
})
}
}
//view to take snapshot of
var otherview: some View {
TestView112()
}
}
I have a SwiftUI that shows two items in a List view. I want to be able push the desired list entry from UIKit. For this purpose, I have created an #State var which I want to set from UIKit to trigger the desired list entry via the binding on the NavigationLink's selection parameter.
public struct DateOptions: View, MiscellaneousHelper {
#State var selectedItem: AppMainFeatureData? = nil
let tabBarController: TabBarController
public var body: some View {
NavigationView {
List(AppMainFeatureData.dateItems, id: \.self) { item in
NavigationLink(destination: DateFeatureController(svc: tabBarController.svc, feature: item),
tag: item, selection: $selectedItem) {
ImageText(text: item.title, imageName: item.iconName)
}
}
.navigationBarTitle(SideBarSection.dates.title)
}
}
}
Here's my UIViewControllerRepresentable:
struct DateFeatureController: UIViewControllerRepresentable, MiscellaneousHelper {
let svc: SplitViewController
let feature: AppMainFeatureData
func makeUIViewController(context: Context) -> UINavigationController {
UINavigationController(rootViewController: feature.viewController)
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
}
}
The is how I create DateOptions in my UITabBar and how it gets added to the viewControllers:
let dateViewController = UIHostingController(rootView: DateOptions(tabBarController: self))
dateViewController.tabBarItem = tabBarItem
viewControllers?.insert(dateViewController, at: 1)
Here's how I set selectedItem from a UITabBar:
if let dateFeature = viewControllers?[selectedIndex] as? UIHostingController<DateOptions> {
dateFeature.rootView.selectedItem = .dateAddSubtract
dPrint("After \(dateFeature.rootView.selectedItem)")
}
But the print shows selectedItem as nil.
Everything works fine when the the user touches a List entry.
How can I set selectedItem from UIKit?
Context
I have created a UIViewRepresentable to wrap a UITextField so that:
it can be set it to become the first responder when the view loads.
the next textfield can be set to become the first responder when enter is pressed
Problem
When used inside a NavigationView, unless the keyboard is dismissed from previous views, the view doesn't observe the value in their ObservedObject.
Question
Why is this happening? What can I do to fix this behaviour?
Screenshots
Keyboard from root view not dismissed:
Keyboard from root view dismissed:
Code
Here is the said UIViewRepresentable
struct SimplifiedFocusableTextField: UIViewRepresentable {
#Binding var text: String
private var isResponder: Binding<Bool>?
private var placeholder: String
private var tag: Int
public init(
_ placeholder: String = "",
text: Binding<String>,
isResponder: Binding<Bool>? = nil,
tag: Int = 0
) {
self._text = text
self.placeholder = placeholder
self.isResponder = isResponder
self.tag = tag
}
func makeUIView(context: UIViewRepresentableContext<SimplifiedFocusableTextField>) -> UITextField {
// create textfield
let textField = UITextField()
// set delegate
textField.delegate = context.coordinator
// configure textfield
textField.placeholder = placeholder
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.tag = self.tag
// return
return textField
}
func makeCoordinator() -> SimplifiedFocusableTextField.Coordinator {
return Coordinator(text: $text, isResponder: self.isResponder)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<SimplifiedFocusableTextField>) {
// update text
uiView.text = text
// set first responder ONCE
if self.isResponder?.wrappedValue == true && !uiView.isFirstResponder && !context.coordinator.didBecomeFirstResponder{
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
private var isResponder: Binding<Bool>?
var didBecomeFirstResponder = false
init(text: Binding<String>, isResponder: Binding<Bool>?) {
_text = text
self.isResponder = isResponder
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = false
}
}
}
}
And to reproduce, here is the contentView:
struct ContentView: View {
var body: some View {
return NavigationView { FieldView(tag: 0) }
}
}
and here's the view with the field and its view model
struct FieldView: View {
#ObservedObject private var viewModel = FieldViewModel()
#State private var focus = false
var tag: Int
var body: some View {
return VStack {
// listen to viewModel's value
Text(viewModel.value)
// text field
SimplifiedFocusableTextField("placeholder", text: self.$viewModel.value, isResponder: $focus, tag: self.tag)
// push to stack
NavigationLink(destination: FieldView(tag: self.tag + 1)) {
Text("Continue")
}
// dummy for tapping to dismiss keyboard
Color.green
}
.onAppear {
self.focus = true
}.dismissKeyboardOnTap()
}
}
public extension View {
func dismissKeyboardOnTap() -> some View {
modifier(DismissKeyboardOnTap())
}
}
public struct DismissKeyboardOnTap: ViewModifier {
public func body(content: Content) -> some View {
return content.gesture(tapGesture)
}
private var tapGesture: some Gesture {
TapGesture().onEnded(endEditing)
}
private func endEditing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
.compactMap({$0})
.first?.windows
.filter {$0.isKeyWindow}
.first?.endEditing(true)
}
}
class FieldViewModel: ObservableObject {
var subscriptions = Set<AnyCancellable>()
// diplays
#Published var value = ""
}
It looks like SwiftUI rendering engine again over-optimized...
Here is fixed part - just make destination unique forcefully using .id. Tested with Xcode 11.4 / iOS 13.4
NavigationLink(destination: FieldView(tag: self.tag + 1).id(UUID())) {
Text("Continue")
}
I am using a SwiftUI TextField with a Binding String to change the user's input into a phone format. Upon typing, the formatting is happening, but the cursor isn't moved to the end of the textfield, it remains on the position it was when it was entered. For example, if I enter 1, the value of the texfield (after formatting) will be (1, but the cursor stays after the first character, instead of at the end of the line.
Is there a way to move the textfield's cursor to the end of the line?
Here is the sample code:
import SwiftUI
import AnyFormatKit
struct ContentView: View {
#State var phoneNumber = ""
let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")
var body: some View {
let phoneNumberProxy = Binding<String>(
get: {
return (self.phoneFormatter.format(self.phoneNumber) ?? "")
},
set: {
self.phoneNumber = self.phoneFormatter.unformat($0) ?? ""
}
)
return TextField("Phone Number", text: phoneNumberProxy)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You might have to use UITextField instead of TextField. UITextField allows setting custom cursor position. To position the cursor at the end of the text you can use textField.endOfDocument to set UITextField.selectedTextRange when the text content is updated.
#objc func textFieldDidChange(_ textField: UITextField) {
let newPosition = textField.endOfDocument
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
The following SwiftUI code snippet shows a sample implementation.
import SwiftUI
import UIKit
//import AnyFormatKit
struct ContentView: View {
#State var phoneNumber = ""
let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")
var body: some View {
let phoneNumberProxy = Binding<String>(
get: {
return (self.phoneFormatter.format(self.phoneNumber) ?? "")
},
set: {
self.phoneNumber = self.phoneFormatter.unformat($0) ?? ""
}
)
return TextFieldContainer("Phone Number", text: phoneNumberProxy)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
/************************************************/
struct TextFieldContainer: UIViewRepresentable {
private var placeholder : String
private var text : Binding<String>
init(_ placeholder:String, text:Binding<String>) {
self.placeholder = placeholder
self.text = text
}
func makeCoordinator() -> TextFieldContainer.Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<TextFieldContainer>) -> UITextField {
let innertTextField = UITextField(frame: .zero)
innertTextField.placeholder = placeholder
innertTextField.text = text.wrappedValue
innertTextField.delegate = context.coordinator
context.coordinator.setup(innertTextField)
return innertTextField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldContainer>) {
uiView.text = self.text.wrappedValue
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldContainer
init(_ textFieldContainer: TextFieldContainer) {
self.parent = textFieldContainer
}
func setup(_ textField:UITextField) {
textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
#objc func textFieldDidChange(_ textField: UITextField) {
self.parent.text.wrappedValue = textField.text ?? ""
let newPosition = textField.endOfDocument
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
}
}
Unfortunately I can't comment on ddelver's excellent answer, but I just wanted to add that for me, this did not work when I changed the bound string.
My use case is that I had a custom text field component used to edit the selected item from a list, so as you change selected item, the bound string changes. This meant that TextFieldContainer's init method was being called whenever the binding changed, but parent inside the Coordinator still referred to the initial parent.
I'm new to Swift so there may be a better fix for this, but I fixed it by adding a method to the Coordinator:
func updateParent(_ parent : TextFieldContainer) {
self.parent = parent
}
and then calling this from func updateUIView like:
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldContainer>) {
uiView.text = self.text.wrappedValue
context.coordinator.updateParent(self)
}
You can do something like this:
final class ContentViewModel: ObservableObject {
private let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")
private var realPhoneNumber = ""
#Published var formattedPhoneNumber = "" {
didSet {
let formattedText = phoneFormatter.format(formattedPhoneNumber) ?? ""
// Need this check to avoid infinite loop
if formattedPhoneNumber != formattedText {
let realText = phoneFormatter.unformat(formattedPhoneNumber) ?? ""
formattedPhoneNumber = formattedText
realPhoneNumber = realText
}
}
}
}
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
return TextField("Phone Number", text: $viewModel.formattedPhoneNumber)
}
}
The idea here is that when you manually set (assign) the text binding, the cursor of the textField moves to the end of the text.
I have a throwaway project I am using to try to familiarize myself with SwiftUI. Essentially, I have various types of apples, that I have made available through an EnvironmentObject variable. The project parallels the Landmarks tutorial that I have been through, but I am expanding on the use of objects such as steppers and buttons, etc.
I am currently attempting to have a button, when pressed, save the UUID of a certain variety of apple and send it back to the original view. It is not working, and I am not sure why. It seems like a problem of the environmentObject assignment not escaping the closure for the action:. Have have set print statements and Text views to display the values of the variables at certain points. While it seems to set the variable in the closure, it doesn't escape the closure and the variable is never really updated.
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserData()))
self.window = window
window.makeKeyAndVisible()
}
}
struct AppleData: Codable, Hashable, Identifiable {
let id: UUID
var appleType: String
var numberOfBaskets: Int
var numberOfApplesPerBasket: [Int]
var fresh: Bool
static let `default` = Self(id: UUID(uuidString: "71190FD1-C8E0-4A65-996E-9CE84D200FBA")!,
appleType: "appleType",
numberOfBaskets: 1,
numberOfApplesPerBasket: [0],
fresh: true) // for purposes of automatic preview
func image(forSize size: Int) -> Image {
ImageStore.shared.image(name: appleType, size: size)
}
}
let appleData: [AppleData] = load("apples.json")
var appleUUID: UUID?
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
... // Code Omitted For Brevity
}
final class UserData: ObservableObject {
let willChange = PassthroughSubject<UserData, Never>()
var apples = appleData {
didSet {
willChange.send(self)
}
}
var appleId = appleUUID {
didSet {
willChange.send(self)
}
}
}
struct ContentView : View {
#EnvironmentObject private var userData: UserData
var body: some View {
NavigationView {
List {
ForEach(appleData) { apple in
NavigationLink(
destination: AppleDetailHost(apple: apple).environmentObject(self.userData)
) {
Text(verbatim: apple.appleType)
}
}
Text("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
}
... // Code Omitted For Brevity
}
}
struct AppleDetail : View {
#EnvironmentObject var userData: UserData
#State private var basketIndex: Int = 0
var apple: AppleData
var totalApples: Int {
apple.numberOfApplesPerBasket.reduce(0, +)
}
var body: some View {
VStack {
... // Code Omitted For Brevity
}
Button(action: {
print("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
self.userData.appleId = self.apple.id
print("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
}) {
Text("Use Apple")
}
Text("self.apple.id: \(self.apple.id.uuidString)")
Text("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
}
... // Code Omitted For Brevity
}
The output of the print statements in the Button in AppleDetail is:
self.userData.appleId: Nil
self.userData.appleId: 28EE7739-5E5A-4CA4-AFF5-7A6BFE025250
The Text view that shows self.userData.appleId in ContentView is always Nil. Any help would be greatly appreciated.
In beta 5, the ObservableObject no longer uses willChange. It uses objectWillChange instead. In addition, it also autosynthesizes the subject, so you do not have to write it yourself (although you could overwrite it if you want).
On top of that, there's a new property wrapper (#Published), that will make changes on a property to have the publisher emit. No need to manually call .send(), as it will be done automatically. So if in your code, you rewrite your UserData class like this, it will work fine:
final class UserData: ObservableObject {
#Published var apples = appleData
#Published var appleId = appleUUID
}