I am currently making an app that takes in three user inputs for a red, green, and blue color value between 0 and 255 through separate TextField views. Right now, the user can input the values fine but there is no way of tabbing out of the keyboards. I understand there are two main ways to go about this. The first is adding a return button to the keyboard and the second is exiting out through a tap gesture.
I am fairly new to SwiftUI and I keep seeing online that the best solution is to override the viewDidLoad function and set a tap gesture in there. I am honestly not sure what the viewDidLoad function is and I am still very confused after researching it. At the moment, I am also not very familiar with UIViewControllers.
Is there an easier way to solve my issue or will I have to use a UIViewController and override the viewDidLoad function?
enter code here
VStack {
TextField("255", text: $redC) { editing in
isEditing = editing
redV = (redC as NSString).doubleValue
} onCommit: {
redV = (redC as NSString).doubleValue
}
TextField("255", text: $greenC) { editing in
isEditing = editing
greenV = (greenC as NSString).doubleValue
} onCommit: {
greenV = (greenC as NSString).doubleValue
}
TextField("255", text: $blueC) { editing in
isEditing = editing
blueV = (blueC as NSString).doubleValue
} onCommit: {
blueV = (blueC as NSString).doubleValue
}
}
.multilineTextAlignment(.center)
.keyboardType(.numberPad)
Use onTapGesture closure
First Create an extension in UIApplication like that,
extension UIApplication {
func resignFirstResponder() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Add this in VStack like that,
VStack {
// contents
}
.onTapGesture {
UIApplication.shared.resignFirstResponder()
}
Related
I am trying to change font size of button itself after it was pressed.
class ViewController: UIViewController {
#IBOutlet weak var buttonToResize: UIButton!
#IBAction func buttonTapped(_ sender: UIButton) {
buttonToResize.titleLabel!.font = UIFont(name: "Helvetica", size: 40)
// Also tried buttonToResize.titleLabel?.font = UIFont .systemFont(ofSize: 4)
}
However the changes are not applied.
What is interesting, to me, that if I try to resize some other button (second one) after pressing on initial (first one), it works as expected.
Like this:
class ViewController: UIViewController {
#IBOutlet weak var buttonToResize: UIButton!
#IBOutlet weak var secondButtonToResize: UIButton!
#IBAction func buttonTapped(_ sender: UIButton) {
secondButtonToResize.titleLabel!.font = UIFont(name: "Helvetica", size: 40)
}
Other properties like backgroundColor seems to apply, however with font size I face problem.
First, here's the sequence of events when you tap on a UIButton.
The button sets its own isHighlighted property to true.
The button fires any Touch Down actions, possibly multiple times if you drag your finger around.
The button fires any Primary Action Triggered actions (like your buttonTapped).
The button fires any Touch Up actions.
The button sets its own isHighlighted property to false.
Every time isHighlighted changes, the button updates its styling to how it thinks it should look. So a moment after buttonTapped, the button you pressed overwrites your chosen font with its own font.
It's worth exploring this to make sure you understand it by creating a UIButton subclass. Don't use this in production. Once you start overriding parts of UIButton, you need to override all of it.
// This class is to demonstrate the sequence of events when you press a UIButton.
// Do not use in production.
// To make this work properly, you would also have to override all the other properties that effect UIButton.state, and also UIButton.state itself.
class MyOverrideHighlightedButton : UIButton {
// Define a local property to store our highlighted state.
var myHighlighted : Bool = false
override var isHighlighted: Bool {
get {
// Just return the existing property.
return myHighlighted
}
set {
print("Setting MyOverrideHighlightedButton.isHighlighted from \(myHighlighted) to \(newValue)")
myHighlighted = newValue
// Since the UIButton remains unaware of its highlighted state changing, we need to change its appearance here.
// Use garish colors so we can be sure it works during testing.
if (myHighlighted) {
titleLabel!.textColor = UIColor.red
} else {
titleLabel!.textColor = titleColor(for: .normal)
}
}
}
}
So where does it keep pulling its old font from? On loading a view it will apply UIAppearance settings, but those will get discarded when you press the button too. iOS 15+, it looks like it uses the new UIButton.Configuration struct. So you could put this in your buttonTapped:
// The Configuration struct used here was only defined in iOS 15 and
// will fail in earlier versions.
// See https://developer.apple.com/documentation/uikit/uibutton/configuration
sender.configuration!.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
var outgoing = incoming
// We only want to change the font, but we could change other properties here too.
outgoing.font = UIFont(name: "Zapfino", size: 20)
return outgoing
}
I'd like to think there's a simpler way to do this. Whichever way you work, make sure it will also work in the event of other changes to your button, such as some other event setting isEnabled to false on it.
You probably want something like this
struct MyView: View {
#State var pressed: Bool = false
var body: some View {
Button(action: {
withAnimation {
pressed = true
}
}) {
Text("Hello")
}
Button(action: {
}) {
Text("Hello")
.font(pressed ? .system(size: 40) : .system(size: 20))
}
}
}
class ViewController: UIViewController {
#IBOutlet weak var buttonToResize: UIButton!
#IBAction func buttonTapped(_ sender: UIButton) {
sender.titleLabel?.font = .systemFont(ofSize: 30)
}
}
This should solve the problem. Use the sender tag instead of the IBOutlet.
Cowirrie analysis made me think of this solution (tested)
#IBAction func testX(_ sender: UIButton) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
sender.titleLabel?.font = sender.titleLabel?.font.withSize(32)
}
}
I have an issue with Xcode 12 / iOS 14. Using multiple NavigationLinks in a sheet with NavigationView leads to NavigationLink entries staying highlighted after going back a page. This is not only a problem with the simulator. See the attached GIF:
Does anybody know how to fix this?
Similar question: SwiftUI - NavigationLink cell in a Form stays highlighted after detail pop (but that's not the problem here).
struct ContentView: View {
var body: some View {
Text("")
.sheet(isPresented: .constant(true), content: {
NavigationView {
Form {
Section {
NavigationLink("Link to ViewB", destination: ViewB())
}
}
.navigationBarTitle("ViewA")
}
})
}
}
struct ViewB: View {
#State var selection = 0
let screenOptions = ["a", "b", "c"]
var body: some View{
Form {
Section {
NavigationLink("Link to ViewC", destination: ViewC())
}
}
.navigationBarTitle("ViewB")
}
}
struct ViewC: View {
var body: some View{
Form {
Section {
Text("Test")
}
}
.navigationBarTitle("ViewC")
}
}
I've also run into this problem when using a NavigationLink inside a sheet. My solution on iOS 14 has been too Swizzle didSelectRowAt: of UITableView. When the row is selected, I deselect it. There is more code for detecting if its in a sheet, etc, but this is the basic, get it working code:
extension UITableView {
#objc static func swizzleTableView() {
guard self == UITableView.self else {
return
}
let originalTableViewDelegateSelector = #selector(setter: self.delegate)
let swizzledTableViewDelegateSelector = #selector(self.nsh_set(delegate:))
let originalTableViewMethod = class_getInstanceMethod(self, originalTableViewDelegateSelector)
let swizzledTableViewMethod = class_getInstanceMethod(self, swizzledTableViewDelegateSelector)
method_exchangeImplementations(originalTableViewMethod!,
swizzledTableViewMethod!)
}
#objc open func nsh_set(delegate: UITableViewDelegate?) {
nsh_set(delegate: delegate)
guard let delegate = delegate else { return }
let originalDidSelectSelector = #selector(delegate.tableView(_:didSelectRowAt:))
let swizzleDidSelectSelector = #selector(self.tableView(_:didSelectRowAt:))
let swizzleMethod = class_getInstanceMethod(UITableView.self, swizzleDidSelectSelector)
let didAddMethod = class_addMethod(type(of: delegate), swizzleDidSelectSelector, method_getImplementation(swizzleMethod!), method_getTypeEncoding(swizzleMethod!))
if didAddMethod {
let didSelectOriginalMethod = class_getInstanceMethod(type(of: delegate), NSSelectorFromString("tableView:didSelectRowAt:"))
let didSelectSwizzledMethod = class_getInstanceMethod(type(of: delegate), originalDidSelectSelector)
if didSelectOriginalMethod != nil && didSelectSwizzledMethod != nil {
method_exchangeImplementations(didSelectOriginalMethod!, didSelectSwizzledMethod!)
}
}
}
#objc open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView(tableView, didSelectRowAt: indexPath)
// This is specifically to fix a bug in SwiftUI, where a NavigationLink is
// not de-selecting itself inside a sheet.
tableView.deselectRow(at: indexPath,
animated: true)
}
}
(Original swizzle code is from https://stackoverflow.com/a/59262109/127853), this code sample just adds the deselectRow call.)
Don't forget to call UITableView.swizzleTableView() somewhere such as application:didFinishLaunchingWithOptions:
Add the following modifier to your NavigationView to set navigation view style and fix this issue:
.navigationViewStyle(StackNavigationViewStyle())
Explanation:
Default style is DefaultNavigationViewStyle(), from documentation: "The default navigation view style in the current context of the view being styled".
For some reason this will pick up DoubleColumnNavigationViewStyle instead of StackNavigationViewStyle on iPhone, if you set style explicitly it behaves as expected.
I created a UIViewRepresentable to wrap UITextField for SwiftUI, so I can e.g. change the first responder when the enter key was tapped by the user.
This is my UIViewRepresentable (I removed the first responder code to keep it simple)
struct CustomUIKitTextField: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: UIViewRepresentableContext<CustomUIKitTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomUIKitTextField>) {
uiView.text = text
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentCompressionResistancePriority(.required, for: .vertical)
}
func makeCoordinator() -> CustomUIKitTextField.Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: CustomUIKitTextField
init(parent: CustomUIKitTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
The first screen of the app has a "Sign in with email" button which pushes MailView that displays a CustomUIKitTextField and uses a #Published property of an ObservableObject view model as the TextField's text.
struct MailView: View {
#ObservedObject var viewModel: MailSignUpViewModel
var body: some View {
VStack {
CustomUIKitTextField(placeholder: self.viewModel.placeholder,
text: self.$viewModel.mailAddress)
.padding(.top, 30)
.padding(.bottom, 10)
NavigationLink(destination: UsernameView(viewModel: UsernameSignUpViewModel())) {
Text("Next")
}
Spacer()
}
}
}
Everything works fine until I push another view like MailView, say e.g. UsernameView. It is implemented exactly in the same way, but somehow the CustomUIKitTextField gets an updateUIView call with an empty string once I finish typing.
There is additional weird behavior like when I wrap MailView and UsernameView in another NavigationView, everything works fine. But that is obviously not the way to fix it, since I would have multiple NavigationViews then.
It also works when using an #State property instead of a #Published property inside a view model. But I do not want to use #State since I really want to keep the model code outside the view.
Is there anybody who faced the same issue or a similar one?
It looks like you’re using the wrong delegate method. textFieldDidChangeSelection will produce some inconsistent results (which is what it sounds like you’re dealing with). Instead, I recommend using textFieldDidEndEditing which will also give you access to the passed in control, but it guarantees that you’re getting the object as it is resigning the first responder. This is important because it means you’re getting the object after the properties have been changed and it’s releasing the responder object.
So, I would change your delegate method as follows:
func textFieldDidEndEditing(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
For more info, see this link for the textFieldDidEndEditing method:
https://developer.apple.com/documentation/uikit/uitextfielddelegate/1619591-textfielddidendediting
And this link for info on the UITextFieldDelegate object:
https://developer.apple.com/documentation/uikit/uitextfielddelegate
EDIT
Based on the comment, if you're looking to examine the text everytime it changes by one character, you should implement this delegate function:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// User pressed the delete key
if string.isEmpty {
// If you want to update your query string here after the delete key is pressed, you can rebuild it like we are below
return true
}
//this will build the full string the user intends so we can use it to build our search
let currentText = textField.text ?? ""
let replacementText = (currentText as NSString).replacingCharacters(in: range, with: string)
// You can set your parent.text here if you like, or you can fire the search function as well
// When you're done though, return true to indicate that the textField should display the changes from the user
return true
}
I also needed a UITextField representation in SwiftUI, which reacts to every character change, so I went with the answer by binaryPilot84. While the UITextFieldDelegate method textField(_:shouldChangeCharactersIn:replacementString:) is great, it has one caveat -- every time we update the text with this method, the cursor moves to the end of the text. It might be desired behavior. However, it was not for me, so I implemented target-action like so:
public func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.addTarget(context.coordinator, action: #selector(context.coordinator.textChanged), for: .editingChanged)
return textField
}
public final class Coordinator: NSObject {
#Binding private var text: String
public init(text: Binding<String>) {
self._text = text
}
#objc func textChanged(_ sender: UITextField) {
guard let text = sender.text else { return }
self.text = text
}
}
It might sound like a trivial task but I can't find a proper solution for this problem. Possibly I haven't internalized the "SwiftUI-ish" way of thinking yet.
I have a view with a button. When the view loads, there is a condition (already logged in?) under which the view should directly go to the next view. If the button is clicked, an API call is triggered (login) and if it was successful, the redirect to the next view should also happen.
My attempt was to have a model (ObservableObject) that holds the variable "shouldRedirectToUploadView" which is a PassThroughObject. Once the condition onAppear in the view is met or the button is clicked (and the API call is successful), the variable flips to true and tells the observer to change the view.
Flipping the "shouldRedirectToUploadView" in the model seems to work but I can't make the view re-evaluate that variable so the new view won't open.
Here is my implementation so far:
The model
import SwiftUI
import Combine
class SboSelectorModel: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
var shouldRedirectToUpdateView = false {
didSet {
didChange.send()
}
}
func fetch(_ text: String) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.shouldRedirectToUpdateView = true
}
}
}
The view
import SwiftUI
struct SboSelectorView: View {
#State var text: String = ""
#ObservedObject var model: SboSelectorModel
var body: some View {
return ZStack {
if (model.shouldRedirectToUpdateView) {
UpdateView()
}
else {
Button(action: {
self.reactOnButtonClick()
}) {
Text("Start")
}
}
}.onAppear(perform: initialActions)
}
public func initialActions() {
self.model.shouldRedirectToUpdateView = true
}
private func reactOnButtonClick() {
self.model.fetch()
}
}
In good old UIKit I would have just used a ViewController to catch the action of button click and then put the new view on the navigation stack. How would I do it in SwiftUI?
In the above example I would expect the view to load, execute the onAppear() function which executes initialActions() to flip the model variable what would make the view react to that change and present the UploadView. Why doesn't it happen that way?
There are SO examples like Programatically navigate to new view in SwiftUI or Show a new View from Button press Swift UI or How to present a view after a request with URLSession in SwiftUI? that suggest the same procedure. However it does not seem to work for me. Am I missing something?
Thank you in advance!
Apple has introduced #Published which does all the model did change stuff.
This works for me and it looks much cleaner.
You can also use .onReceive() to perform stuff on a view when something in your model changes.
class SboSelectorModel: ObservableObject {
#Published var shouldRedirectToUpdateView = false
func fetch(_ text: String) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.shouldRedirectToUpdateView = true
}
}
}
struct UpdateView: View {
var body: some View {
Text("Hallo")
}
}
struct SboSelectorView: View {
#State var text: String = ""
#ObservedObject var model = SboSelectorModel()
var body: some View {
ZStack {
if (self.model.shouldRedirectToUpdateView) {
UpdateView()
}
else {
Button(action: {
self.reactOnButtonClick()
}) {
Text("Start")
}
}
}.onAppear(perform: initialActions)
}
public func initialActions() {
self.model.shouldRedirectToUpdateView = true
}
private func reactOnButtonClick() {
self.model.fetch("")
}
}
I hope this helps.
EDIT
So this seems to have changed in beta 5
Here a working model with PassthroughSubject:
class SboSelectorModel: ObservableObject {
let objectWillChange = PassthroughSubject<Bool, Never>()
var shouldRedirectToUpdateView = false {
willSet {
objectWillChange.send(shouldRedirectToUpdateView)
}
}
func fetch(_ text: String) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.shouldRedirectToUpdateView = true
}
}
}
Just in case any wants an alternative to the SwiftUI way of having Observable Objects and the like - which can be great, but as I was building out, I noticed I had like, 100 objects and didn't like in the slightest how complicated it all felt. (Oh, how I wanted to just type self.present("nextScene", animated: true)). I know a large part of this is my mind just not up to that SwiftUI life yet but just in case anyone else wants a more... UIKit meets SwiftUI alternative, here's a system that works.
I'm not a professional so I don't know if this is the best memory management way.
First, create a function that allows you to know what the top view controller is on the screen. The code below was borrowed from db0Company on GIT.
import UIKit
extension UIViewController {
func topMostViewController() -> UIViewController {
if let presented = self.presentedViewController {
return presented.topMostViewController()
}
if let navigation = self.presentedViewController as? UINavigationController {
return navigation.visibleViewController?.topMostViewController() ?? navigation
}
if let tab = self as? UITabBarController {
return tab.selectedViewController?.topMostViewController() ?? tab
}
return self
}
}
extension UIApplication {
func topMostViewController() -> UIViewController? {
return self.keyWindow?.rootViewController?.topMostViewController()
}
}
Create an enum - now this is optional, but I think very helpful - of your SwiftUI and UIViewControllers; for demonstration purposes, I have 2.
enum RootViews {
case example, welcome
}
Now, here's some fun; create a delegate you can call from your SwiftUI views to move you from scene to scene. I call mine Navigation Delegate.
I added some default presentation styles here, to make calls easier via the extension.
import UIKit //SUPER important!
protocol NavigationDelegate {
func moveTo(view: RootViews, presentation: UIModalPresentationStyle, transition: UIModalTransitionStyle)
}
extension NavigationDelegate {
func moveTo(view: RootViews) {
self.moveTo(view: view, presentation: .fullScreen, transition: .crossDissolve)
}
func moveTo(view: RootViews, presentation: UIModalPresentationStyle) {
self.moveTo(view: view, presentation: presentation, transition: .crossDissolve)
}
func moveTo(view: RootViews, transition: UIModalTransitionStyle) {
self.moveTo(view: view, presentation: .fullScreen, transition: transition)
}
}
And here, I create a RootViewController - a classic, Cocoa Touch Class UIViewController. This will conform to the delegate, and be where we actually move screens.
class RootViewController: UIViewController, NavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.moveTo(view: .welcome) //Which can always be changed
}
//The Moving Function
func moveTo(view: RootViews, presentation: UIModalPresentationStyle = .fullScreen, transition: UIModalTransitionStyle = .crossDissolve) {
let newScene = self.returnSwiftUIView(type: view)
newScene.modalPresentationStyle = presentation
newScene.modalTransitionStyle = transition
//Top View Controller
let top = self.topMostViewController()
top.present(newScene, animated: true)
}
//Swift View switch. Optional, but my Xcode was not happy when I tried to return a UIHostingController in line.
func returnSwiftUIView(type: RootViews) -> UIViewController {
switch type {
case .welcome:
return UIHostingController(rootView: WelcomeView(delegate: self))
case .example:
return UIHostingController(rootView: ExampleView(delegate: self))
}
}
}
So now, when you create new SwiftUI Views, you just need to add the Navigation Delegate, and call it when a button is pressed.
import SwiftUI
import UIKit //Very important! Don't forget to import UIKit
struct WelcomeView: View {
var delegate: NavigationDelegate?
var body: some View {
Button(action: {
print("full width")
self.delegate?.moveTo(view: .name)
}) {
Text("NEXT")
.frame(width: UIScreen.main.bounds.width - 20, height: 50, alignment: .center)
.background(RoundedRectangle(cornerRadius: 15, style: .circular).fill(Color(.systemPurple)))
.accentColor(.white)
}
}
And last but not least, in your scene delegate, create your RootViewController() and use that as your key, instead of the UIHostingController(rootView: contentView).
Voila.
I hope this can help someone out there! And for my more professional senior developers out there, if you can see a way to make it... cleaner? Or whatever it is that makes code less bad, feel free!
Ok so I have a function that allows my user to use the keyboard to go to the next field (Which I got the code from SO) It works perfect. My problem is, once my user gets to the final text field, in which, I've selected "GO" as the return button, and I want to use the go as the ideal submit button. As the flow through the form goes, its presentably right, but the functionality at the end isn't there. I found an answer here, but it looks like its calling the same functions as the "next field" code. So they don't work well together. So here's what the Next Field code looks like:
override func viewDidLoad() {
super.viewDidLoad()
// Keyboard Next Field & Delegate
enterEmailTextField.delegate = self
enterPasswordTextField.delegate = self
self.enterEmailTextField.nextField = self.enterPasswordTextField
self.enterPasswordTextField.nextField = self.enterNameTextField
// ...
}
And the next block is displayed below override (which i'm sure you know) func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() }
// Next Field
func textFieldShouldReturn(textField: UITextField) -> Bool {
if let nextField = textField.nextField {
nextField.becomeFirstResponder()
}
return true
}
Ok that works just fine down to the last text field, But since the last field is "Go" I'm trying to mimic what I would do with say an #IBAction for a button, but instead the go button. Here's where I got the idea/code for the Go to work:
Action of the "Go" button of the ios keyboard
Any Ideas on how to implement these? Or maybe just to work with the next function, and implement a "keyboard go" action similar to an #IBaction? Maybe just an all around better way to implement both of these so they coincide with each other? Any help is much appreciated!
EDIT!
I forgot the actual NextField.swift File which I'm sure is important (sorry)
import Foundation
import UIKit
private var kAssociationKeyNextField: UInt8 = 0
extension UITextField {
#IBOutlet var nextField: UITextField? {
get {
return objc_getAssociatedObject(self, &kAssociationKeyNextField) as? UITextField
}
set(newField) {
objc_setAssociatedObject(self, &kAssociationKeyNextField, newField, UInt(OBJC_ASSOCIATION_RETAIN))
}
}
}
this would help a lot I'm assuming
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
if (textField.returnKeyType == UIReturnKeyNext) {
// tab forward logic here
return YES;
}
else if (textField.returnKeyType == UIReturnKeyGo) {
// submit action here
return YES;
}
return NO;
}
On a cleaner way to handle tab order and form submitting, read my answer here.
First set return key to Go of your TextField, then implement the following method:
func textFieldShouldReturn(textField: UITextField) -> Bool {
if (textField.returnKeyType == UIReturnKeyType.Go)
{
// Implement your IBAction method here
}
return true
}
then see in below image:
Available for iOS 15 in SwiftUI. You can use .submitLabel(.go) to do it.
#available(iOS 15.0, *)
struct TextFieldSubmitView: View {
#Binding var name: String
var body: some View {
VStack(alignment: .leading) {
TextField("Type text", text: $name)
.textFieldStyle(.roundedBorder)
.padding()
#if os(iOS)
.submitLabel(.go)
#endif
}
}
}
#available(iOS 15.0, *)
struct TextFieldSubmitView_Previews: PreviewProvider {
static var previews: some View {
TextFieldSubmitView(name: .constant("Name"))
.previewLayout(.sizeThatFits)
}
}