How to Change NSTextField Font Size When Used in SwiftUI - swiftui

Below is a demo app with a simple NSTextField in a Mac app. For some reason, the font size won't change no matter what I try.
import Cocoa
import SwiftUI
#main
struct SwiftUIWindowTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State private var text = "Text goes here..."
var body: some View {
FancyTextField(text: $text)
.padding(50)
}
}
struct FancyTextField: NSViewRepresentable {
#Binding var text: String
func makeNSView(context: Context) -> NSTextField {
let textField = NSTextField()
textField.font = NSFont.systemFont(ofSize: 30) //<- Not working
return textField
}
func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.stringValue = text
}
}
That's the whole app. I'm not doing anything else in this simple example. I can change the text color and other attributes, but for some reason the font size doesn't change.
On a whim, I tried changing it on the SwiftUI side as well, but I didn't expect that to work (and it doesn't):
FancyTextField(text: $text)
.font(.system(size: 20))
Any ideas?

This is a particularly weird one:
struct FancyTextField: NSViewRepresentable {
#Binding var text: String
func makeNSView(context: Context) -> NSTextField {
let textField = MyNSTextField()
textField.customSetFont(font: .systemFont(ofSize: 50))
return textField
}
func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.stringValue = text
}
}
class MyNSTextField : NSTextField {
func customSetFont(font: NSFont?) {
super.font = font
}
override var font: NSFont? {
get {
return super.font
}
set {}
}
}
Maybe someone will come up with a cleaner solution to this, but you're right -- the normal methods for just setting .font on the NSTextField do not seem to work here. It seems to be because outside elements (the debugger doesn't give me a good hint) are trying to set the font to system font size 13.
So, my solution is to subclass NSTextField and make the setter for font not responsive. Then, I define a custom setter method, so the only way to get up to the real setter is through my custom method.
A little hacky, but it works.

Related

How can I assign value of some View to type View in SwiftUI?

I have a simple View called TextView, the type of TextView is naturally View, I want make a modification on TextView like .foregroundColor(Color.red) and replace it again as itself! from my understanding my TextView is type View and should just working because the type is not Text But also I understand the complaining from SwiftUI which says cannot assign value of some View to type View. I like make some correction for solving issue, also I am not interested to add another initializer as foregroundColor.
import SwiftUI
struct ContentView: View {
#State private var myTextView: TextView?
var body: some View {
if let unwrappedMyTextView = myTextView {
unwrappedMyTextView
}
Button ("update") {
myTextView = TextView(stringOfText: "Hello, world!")//.foregroundColor(Color.red)
}
.padding()
}
}
struct TextView: View {
let stringOfText: String
init(stringOfText: String) {
self.stringOfText = stringOfText
}
var body: some View {
return Text(stringOfText)
}
}
The problem is that foregroundColor doesn't return TextView:
#inlinable public func foregroundColor(_ color: Color?) -> some View
Which means that
myTextView = TextView(stringOfText: "Hello, world!").foregroundColor(Color.red)
is no longer a TextView but some other View which is a result of applying foregroundColor.
Also, you shouldn't hold View properties as #State.
Try this instead:
struct ContentView: View {
var body: some View {
myTextView
}
var myTextView: some View {
TextView(stringOfText: "Hello, world!")
.foregroundColor(Color.red)
}
}

SwiftUI changing text title of navigationBar Button at the current view to "Back" instead of inheriting the text from previous view title

I have a swiftUI view with navigation links that when i click will navigate to another view . The issue is the second view navigationBa button title still has the title of the previous view instead of a logical back title . How i can have the title as Back with changing the title as "Back" in the first view .
First view navigationBar code: The second view just shows the news website in a WebView.
.navigationBarTitle("Breaking News")
The way i tried is changing the title to this:
.navigationBarTitle("Back")
This will work but the title of the first view changes to "Back" Instead of "Breaking News"
Is there any way i can fix this
An alternative approach is to hide the back button and create your own back button like this:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination : SomeView()) {
Text("Open")
}
.navigationBarTitle("Breaking News")
}
}
}
// Use navigationBarItems for creating your own bar item.
struct SomeView : View {
#Environment(\.presentationMode) var mode
var body : some View {
Text("Hello, World!")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading:
Button(action : {
self.mode.wrappedValue.dismiss()
}){
Text("\(Image(systemName: "chevron.left"))Back")
})
}
}
The accepted answer looks glitchy, it removes a lot of standard behaviour and animations, including long press gesture.
Consider using custom backBarButtonTitle modifier:
struct FirstView: View {
var body: some View {
Text("First view")
.backBarButtonTitle("Back")
}
}
It sets the backButtonTitle property to the topmost UINavigationItem in stack. Be sure to use this modifier on the view, whose title you want to change, see documentation.
Here is the implementation of the backBarButtonTitle:
import SwiftUI
import UIKit
extension View {
func backBarButtonTitle(_ title: String) -> some View {
modifier(BackButtonModifier(title: title))
}
}
// MARK: - BackButtonModifier
struct BackButtonModifier: ViewModifier {
let title: String
func body(content: Content) -> some View {
content.background(BackButtonTitleView(title: title))
}
}
// MARK: - BackButtonTitleView
private struct BackButtonTitleView: UIViewRepresentable {
let title: String
func makeUIView(context _: Context) -> BackButtonTitleUIView {
BackButtonTitleUIView(title: title)
}
func updateUIView(_: BackButtonTitleUIView, context _: Context) {}
}
// MARK: - BackButtonTitleUIView
private final class BackButtonTitleUIView: UIView {
// MARK: Lifecycle
init(title: String) {
self.title = title
super.init(frame: .zero)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
override func layoutSubviews() {
super.layoutSubviews()
if didConfigureTitle {
return
}
let topNavigationItem = searchNavigationController(currentResponder: self)?
.topViewController?
.navigationItem
if let topNavigationItem {
topNavigationItem.backButtonTitle = title
didConfigureTitle = true
}
}
// MARK: Private
private let title: String
private var didConfigureTitle = false
private func searchNavigationController(currentResponder: UIResponder) -> UINavigationController? {
if let navigationController = currentResponder as? UINavigationController {
return navigationController
} else if let nextResponder = currentResponder.next {
return searchNavigationController(currentResponder: nextResponder)
} else {
return nil
}
}
}

Why SwiftUI-transition does not work as expected when I use it in UIHostingController?

I'm trying to get a nice transition for a view that needs to display date. I give an ID to the view so that SwiftUI knows that it's a new label and animates it with transition. Here's the condensed version without formatters and styling and with long duration for better visualisation:
struct ContentView: View {
#State var date = Date()
var body: some View {
VStack {
Text("\(date.description)")
.id("DateLabel" + date.description)
.transition(.slide)
.animation(.easeInOut(duration: 5))
Button(action: { date.addTimeInterval(24*60*60) }) {
Text("Click")
}
}
}
}
Result, it's working as expected, the old label is animating out and new one is animating in:
But as soon as I wrap it inside UIHostingController:
struct ContentView: View {
#State var date = Date()
var body: some View {
AnyHostingView {
VStack {
Text("\(date.description)")
.id("DateLabel" + date.description)
.transition(.slide)
.animation(.easeInOut(duration: 5))
Button(action: { date.addTimeInterval(24*60*60) }) {
Text("Click")
}
}
}
}
}
struct AnyHostingView<Content: View>: UIViewControllerRepresentable {
typealias UIViewControllerType = UIHostingController<Content>
let content: Content
init(content: () -> Content) {
self.content = content()
}
func makeUIViewController(context: Context) -> UIHostingController<Content> {
let vc = UIHostingController(rootView: content)
return vc
}
func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: Context) {
uiViewController.rootView = content
}
}
Result, the new label is not animated in, rather it's just inserted into it's final position, while the old label is animating out:
I have more complex hosting controller but this demonstrates the issue. Am I doing something wrong with the way I update the hosting controller view, or is this a bug in SwiftUI, or something else?
State do not functioning well between different hosting controllers (it is not clear if this is limitation or bug, just empirical observation).
The solution is embed dependent state inside hosting view. Tested with Xcode 12.1 / iOS 14.1.
struct ContentView: View {
var body: some View {
AnyHostingView {
InternalView()
}
}
}
struct InternalView: View {
#State private var date = Date() // keep relative state inside
var body: some View {
VStack {
Text("\(date.description)")
.id("DateLabel" + date.description)
.transition(.slide)
.animation(.easeInOut(duration: 5))
Button(action: { date.addTimeInterval(24*60*60) }) {
Text("Click")
}
}
}
}
Note: you can also experiment with ObservableObject/ObservedObject based view model - that pattern has different life cycle.

Highlight SwiftUI Button programmatically

I've been trying to highlight programmatically a SwiftUI Button, without success so far…
Of course I could implement the whole thing again, but I'd really like to take advantage of what SwiftUI already offers. I would imagine that there is an environment or state variable that we can mutate, but I couldn't find any of those.
Here's a small example of what I would like to achieve:
struct ContentView: View {
#State private var highlighted = false
var body: some View {
Button("My Button", action: doSomething())
.highlighted($highlighted) // <-- ??
Button("Toggle highlight") {
highlighted.toggle()
}
}
func doSomething() { ... }
}
It seems very odd that something so simple if not within easy reach in SwiftUI. Has anyone found a solution?
Here is a demo of possible approach, based on custom ButtonStyle. Tested with Xcode 11.4 / iOS 13.4
struct HighlightButtonStyle: ButtonStyle {
let highlighted: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(highlighted || configuration.isPressed ? Color.yellow : Color.clear)
}
}
extension Button {
func highlighted(_ flag: Bool) -> some View {
self.buttonStyle(HighlightButtonStyle(highlighted: flag))
}
}
struct ContentView: View {
#State private var highlighted = false
var body: some View {
VStack {
Button("My Button", action: doSomething)
.highlighted(highlighted)
Divider()
Button("Toggle highlight") {
self.highlighted.toggle()
}
}
}
func doSomething() { }
}

Disable drag to dismiss in SwiftUI Modal

I've presented a modal view but I would like the user to go through some steps before it can be dismissed.
Currently the view can be dragged to dismiss.
Is there a way to stop this from being possible?
I've watched the WWDC Session videos and they mention it but I can't seem to put my finger on the exact code I'd need.
struct OnboardingView2 : View {
#Binding
var dismissFlag: Bool
var body: some View {
VStack {
Text("Onboarding here! πŸ™ŒπŸΌ")
Button(action: {
self.dismissFlag.toggle()
}) {
Text("Dismiss")
}
}
}
}
I currently have some text and a button I'm going to use at a later date to dismiss the view.
iOS 15+
Starting from iOS 15 we can use interactiveDismissDisabled:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
We just need to attach it to the sheet. Here is an example from the documentation:
struct PresentingView: View {
#Binding var showTerms: Bool
var body: some View {
AppContents()
.sheet(isPresented: $showTerms) {
Sheet()
}
}
}
struct Sheet: View {
#State private var acceptedTerms = false
var body: some View {
Form {
Button("Accept Terms") {
acceptedTerms = true
}
}
.interactiveDismissDisabled(!acceptedTerms)
}
}
It is easy if you use the 3rd party lib Introspect, which is very useful as it access the corresponding UIKit component easily. In this case, the property in UIViewController:
VStack { ... }
.introspectViewController {
$0.isModalInPresentation = true
}
Not sure this helps or even the method to show the modal you are using but when you present a SwiftUI view from a UIViewController using UIHostingController
let vc = UIHostingController(rootView: <#your swiftUI view#>(<#your parameters #>))
you can set a modalPresentationStyle. You may have to decide which of the styles suits your needs but .currentContext prevents the dragging to dismiss.
Side note:I don't know how to dismiss a view presented from a UIHostingController though which is why I've asked a Q myself on here to find out πŸ˜‚
I had a similar question here
struct Start : View {
let destinationView = SetUp()
.navigationBarItem(title: Text("Set Up View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Set Up")
}
}
}
}
The main thing here is that it is hiding the back button. This turns off the back button and makes it so the user can't swipe back ether.
For the setup portion of your app you could create a new SwiftUI file and add a similar thing to get home, while also incorporating your own setup code.
struct SetUp : View {
let destinationView = Text("Your App Here")
.navigationBarItem(title: Text("Your all set up!"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Done")
}
}
}
}
There is an extension to make controlling the modal dismission effortless, at https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
A temporary solution before the official solution released by Apple.
/// Example:
struct ContentView: View {
#State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}