SwiftUI Line break is Weird [duplicate] - swift

I have a text field like this
Text("Hello, one two three four five six seven eight!")
.frame(width:270)
.border(.blue)
When it renders it decides to put seven and eight on the second line even though there is space for seven on the first line. Worse it decides to indent the truncated top line so it is centred within the frame.
How do I fix this so it wraps the text properly without taking into account the orphan?
Edit: Forgot to mention that I wanted this on macOS. I have tried to port it to the Mac. It does correctly left align the text but it doesn't wrap to the second line. The height of the box does get calculated accordingly though.
Here is my updated code:
struct NonOrphanedText: View
{
var text: String
#State private var height: CGFloat = .zero
var body: some View
{
InternalLabelView(text: text, dynamicHeight: $height)
.frame(maxHeight: height)
}
struct InternalLabelView: NSViewRepresentable
{
var text: String
#Binding var dynamicHeight: CGFloat
func makeNSView(context: Context) -> NSTextField
{
let label = NSTextField()
label.isEditable = false
label.isBezeled = false
label.drawsBackground = false
label.isSelectable = false
label.maximumNumberOfLines = 5
label.usesSingleLineMode = false
label.lineBreakStrategy = .init()
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}
func updateNSView(_ nsView: NSTextField, context: Context)
{
nsView.stringValue = text
DispatchQueue.main.async
{
dynamicHeight = nsView.sizeThatFits(CGSize(width: nsView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
}

We need lineBreakStrategy but it is unavailable for now in SwiftUI, so possible solution is to use UILabel.
Here is a possible solution. Tested with Xcode 13.2 / iOS 15.2
struct LabelView: View {
var text: String
#State private var height: CGFloat = .zero
var body: some View {
InternalLabelView(text: text, dynamicHeight: $height)
.frame(maxHeight: height)
}
struct InternalLabelView: UIViewRepresentable {
var text: String
#Binding var dynamicHeight: CGFloat
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakStrategy = .init() // << here !!
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
DispatchQueue.main.async {
dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
}

Related

Why UILabelViewRepresentable does not respect to given frame?

I have a simple UILabelViewRepresentable and I gave a frame to it! But after using in SwiftUI it became max size! How could I solve the issue from UIKit code part?
struct ContentView: View {
var body: some View {
UILabelViewRepresentable(configuration: { label in
label.text = "Hello, World!"
label.textAlignment = .center
label.backgroundColor = .blue
label.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
})
}
}
struct UILabelViewRepresentable: UIViewRepresentable {
let configuration: (UILabel) -> ()
func makeUIView(context: Context) -> UILabel {
return UILabel()
}
func updateUIView(_ uiView: UILabel, context: Context) {
configuration(uiView)
}
}
The main issue is that this representable will try constrain the UILabel to fill the whole screen. You can avoid this by making a UIView, and making the UILabel a child of that so you can center it in the parent.
Code:
struct UILabelViewRepresentable: UIViewRepresentable {
let configuration: (UILabel) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.addSubview(UILabel())
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
let label = uiView.subviews.first! as! UILabel
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: uiView.centerXAnchor),
label.centerYAnchor.constraint(equalTo: uiView.centerYAnchor)
])
configuration(label)
}
}
Usage:
struct ContentView: View {
var body: some View {
UILabelViewRepresentable(configuration: { label in
label.text = "Hello, World!"
label.textAlignment = .center
label.backgroundColor = .blue
label.sizeToFit()
})
}
}
Result:
Depending on your usage, the label.sizeToFit() can be moved inside the UILabelViewRepresentable.
struct ContentView: View {
var body: some View {
UILabelViewRepresentable(text: "Hello, World!",
center: .center,
frame: CGRect(x: 0,
y: 0,
width: 200,
height: 50),
background: .blue)
}
}
struct UILabelViewRepresentable: UIViewRepresentable {
let text : String
let center: NSTextAlignment
let frame: CGRect
let background: UIColor
func makeUIView(context: Context) -> UIView {
let view = UIView()
let label = UILabel()
label.text = text
label.textAlignment = center
label.backgroundColor = background
label.frame = frame
view.addSubview(label)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}

Swift macOS popover detect change dark mode

I have a popover as seen from the image.
I have to make sure that when the screen mode changes, dark mode or light mode, the color of the popover changes.
The color is taken from the asset, like this:
NSColor(named: "backgroundTheme")?.withAlphaComponent(1)
As you can see from the code when starting the popover in the init function I assign the color accordingly.
How can I intercept the change of mode?
Can you give me a hand?
AppDelegate:
import Cocoa
import SwiftUI
#main
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
var statusBar: StatusBarController?
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView()
popover.contentSize = NSSize(width: 560, height: 360)
popover.contentViewController = NSHostingController(rootView: contentView)
statusBar = StatusBarController.init(popover)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
StatusBarController:
import AppKit
import SwiftUI
extension NSPopover {
private struct Keys {
static var backgroundViewKey = "backgroundKey"
}
private var backgroundView: NSView {
let bgView = objc_getAssociatedObject(self, &Keys.backgroundViewKey) as? NSView
if let view = bgView {
return view
}
let view = NSView()
objc_setAssociatedObject(self, &Keys.backgroundViewKey, view, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
NotificationCenter.default.addObserver(self, selector: #selector(popoverWillOpen(_:)), name: NSPopover.willShowNotification, object: nil)
return view
}
#objc private func popoverWillOpen(_ notification: Notification) {
if backgroundView.superview == nil {
if let contentView = contentViewController?.view, let frameView = contentView.superview {
frameView.wantsLayer = true
backgroundView.frame = NSInsetRect(frameView.frame, 1, 1)
backgroundView.autoresizingMask = [.width, .height]
frameView.addSubview(backgroundView, positioned: .below, relativeTo: contentView)
}
}
}
var backgroundColor: NSColor? {
get {
if let bgColor = backgroundView.layer?.backgroundColor {
return NSColor(cgColor: bgColor)
}
return nil
}
set {
backgroundView.wantsLayer = true
backgroundView.layer?.backgroundColor = newValue?.cgColor
}
}
}
class StatusBarController {
private var popover: NSPopover
private var statusBar: NSStatusBar
var statusItem: NSStatusItem
init(_ popover: NSPopover) {
self.popover = popover
self.popover.backgroundColor = NSColor(named: "backgroundTheme")?.withAlphaComponent(1)
statusBar = NSStatusBar.init()
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let statusBarButton = statusItem.button {
statusBarButton.image = #imageLiteral(resourceName: "Fork")
statusBarButton.image?.size = NSSize(width: 18.0, height: 18.0)
statusBarButton.image?.isTemplate = true
statusBarButton.action = #selector(togglePopover(sender:))
statusBarButton.target = self
statusBarButton.imagePosition = NSControl.ImagePosition.imageLeft
}
}
#objc func togglePopover(sender: AnyObject) {
if(popover.isShown) {
hidePopover(sender)
}else {
showPopover(sender)
}
}
func showPopover(_ sender: AnyObject) {
if let statusBarButton = statusItem.button {
popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
}
}
func hidePopover(_ sender: AnyObject) {
popover.performClose(sender)
}
}
Hi I would skip setting the color on the popover and instead set the background in your ContentView.swift
Then set the background to a VStack/HStack/ZStack wrapping the rest of the UI.
var body: some View {
VStack{
Text("Hello, world!").padding()
Button("Ok", action: {}).padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color("backgroundTheme").opacity(0.3))
.padding(.top, -16)
}
There are something to keep in mind:
Methods like .withAlphaComponent(_:) that transform a existing NSColor to a new color does not return a dynamic color.
CGColor is not dynamic-capable. When converting a NSColor to a CGColor using .cgColor, you are converting from the "current" color of a NSColor.
So your hacky way is not really a good approach to what you want.
Base on what you had said, if I understand correctly, you want to add a color overlay to the popover's background, including the arrow portion.
You can actually do all that in a view controller:
class PopoverViewController: NSViewController {
/// for color overlay
lazy var backgroundView: NSBox = {
// 1. This extend the frame to cover arrow potion.
let box = NSBox(frame: view.bounds.insetBy(dx: -13, dy: -13))
box.autoresizingMask = [.width, .height]
box.boxType = .custom
box.titlePosition = .noTitle
box.fillColor = NSColor(named: "backgroundTheme")
return box
}()
/// for mounting SwiftUI views
lazy var contentView: NSView = {
let view = NSView(frame: view.bounds)
view.autoresizingMask = [.width, .height]
return view
}()
override func loadView() {
view = NSView()
// 2. This avoid clipping.
view.wantsLayer = true
view.layer?.masksToBounds = false
view.addSubview(backgroundView)
view.addSubview(contentView)
}
}
Pay attention to 1 and 2, this allow backgroundView to draw beyond view's bounds, covering the arrow portion. backgroundView is a NSBox object that accept a dynamic NSColor object to style its background.
Notice that if you want to change the opacity of the color, instead of .withAlphaComponent(_:), change the opacity on your assets, right below the RGB sliders.
contentView is here as a mounting point for your SwiftUI views. To mount content from a NSHostingController, you can do:
let popoverViewController = PopoverViewController()
_ = popoverViewController.view // this trigger `loadView()`, you don't need this for auto layout
let hostingController = NSHostingController(rootView: ContentView())
hostingController.view.frame = popoverViewController.contentView.bounds
hostingController.view.autoresizingMask = [.width, .height]
popoverViewController.contentView.addSubview(hostingController.view)
popoverViewController.addChild(hostingController)
This add hostingController's view as a subview of popoverViewController's contentView.
That's it.
Do note that I use autoresizingMask instead of auto layout and extract the mounting part out of the PopoverViewController to simply my answer.

TextAlignmentStyle .natural doesn't work with SwiftUI

I need my text to change depend on text language not depend on App language
if it's RTL language like Arabic the text alignment should be RTL
if it's LTR language like English the text alignment should be LTR
I tried to import UILabel from UIKit because SwiftUI doesn't have .natural and .justified
like this
struct LabelAlignment: UIViewRepresentable {
var text: String
var textAlignmentStyle : TextAlignmentStyle
var width: CGFloat
var fontSize: CGFloat
var textColorName: String
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.textAlignment = NSTextAlignment(rawValue: textAlignmentStyle.rawValue)!
label.numberOfLines = 0
label.preferredMaxLayoutWidth = width
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentHuggingPriority(.required, for: .vertical)
label.textColor = UIColor(named: textColorName)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
if let customFont = UIFont(name: "Almarai-Regular", size: fontSize) {
uiView.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: customFont)
uiView.adjustsFontForContentSizeCategory = true
} }
}
enum TextAlignmentStyle : Int{
case left = 0 ,center = 1 , right = 2 ,justified = 3 ,natural = 4
}
then I use it like this
HStack {
LabelAlignment(text: "My text" , textAlignmentStyle: . natural, width:UIScreen.main.bounds.width - 30, fontSize: 15, textColorName: "color3")
.padding(.horizontal,5)
.layoutPriority(1.0)
.foregroundColor(.textColor3)
.regularStyle(size: 15)
.padding([.bottom,.top], 8)
}
it working great with left ,center , right and justified
but it won't work with natural
As Adam mention
if you follow my old answer it won't work correctly if you change the App language
The correct way is
struct LabelAlignment: UIViewRepresentable {
var text: String
var width: CGFloat
var fontSize: CGFloat
var textColorName: String
var detectLangauge = ""
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
if detectLangauge == "Arabic" || detectLangauge == "العربية" {
label.textAlignment = .right
}else {
label.textAlignment = .left
}
label.numberOfLines = 0
label.preferredMaxLayoutWidth = width
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentHuggingPriority(.required, for: .vertical)
label.textColor = UIColor(named: textColorName)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
if let customFont = UIFont(name: "Almarai-Regular", size: fontSize) {
uiView.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: customFont)
uiView.adjustsFontForContentSizeCategory = true
} }
}
Then use it like this
LabelAlignment(text: "My text", width: UIScreen.main.bounds.width - 30, fontSize: 15, textColorName: "color3",detectLangauge:detectLangauge)
it will work as .natural
Old answer :
I found a solution with pure SwiftUI
First you need to add these code
import Foundation
import NaturalLanguage
extension String {
func detectedLanguage() -> String? {
let recognizer = NLLanguageRecognizer()
recognizer.processString(self)
guard let languageCode = recognizer.dominantLanguage?.rawValue else { return nil }
let detectedLanguage = Locale.current.localizedString(forIdentifier: languageCode)
return detectedLanguage
}
}
then you need a #State value to save the result of detectedLanguage()
#State private var detectLangauge = ""
after that check the result of text by using
detectLangauge = "your text".detectedLanguage()
you can check your text language onApper() or after get result from API
then you need to add
Text("Your Text")
.multilineTextAlignment(detectLangauge == "Arabic" ? .trailing : .leading)

SwiftUI Breaking line in UITextView wrapped by UIViewRepresentable

I'm struggling for breaking line in CustomUITextView. Please help...
I made CustomUITextView like below.
struct CustomTextView: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<CustomTextView>) -> UITextView {
let textView = UITextView()
textView.backgroundColor = UIColor.clear
textView.isScrollEnabled = false
textView.textColor = UIColor.black
textView.font = UIFont(name: "ArialMT", size: 20)
// textView.text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) { }
}
Then using it like this.
struct ContentView: View {
var body: some View {
ZStack {
CustomTextView()
.frame(width:300, height: 300, alignment: .topLeading)
.border(Color.red, width: 1)
}
}
}
By keyboard input it works and lines are returned at red border.
But when I take away this comment in CustomUITextView
textView.text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
It will be like this and doesn't break.
Is it possible to break line when I put Strings directly from code like above too ?
Does anyone know the solutions?
You need to decrease compression resistance priority
textView.font = UIFont(name: "ArialMT", size: 20)
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
return textView

How do I create a Multi Line Text Field in SwiftUI for MacOS?

I am writing my first MacOS app using Swift. I need to create a simple editable text box which allows multiple paragraphs. In HTML, this would be a textarea.
I gather that iOS 14 will include TextEditor, but that’s not now, and I don’t know whether that will be in MacOS anyway.
Many solutions I have seen presume iOS and UIKit.
How do I do this with SwiftUI and MacOS?
Update
Someone has suggested that the question is similar to this one: How do I create a multiline TextField in SwiftUI?
I have already looked at that question, but:
The linked question is specifically for iOS, but mine is for MacOS
The answers, as far as I can tell, are specifically for iOS using UIKit. If someone cares to explain how this answers my MacOS question I would be very interested …
Here is some initial demo of component like iOS14 TextEditor.
Demo prepared & tested with Xcode 11.7 / macOS 10.15.6
struct TestTextArea: View {
#State private var text = "Placeholder: Enter some text"
var body: some View {
VStack {
TextArea(text: $text)
.border(Color.black)
// Text(text) // uncomment to see mirror of enterred text
}.padding()
}
}
struct TextArea: NSViewRepresentable {
#Binding var text: String
func makeNSView(context: Context) -> NSScrollView {
context.coordinator.createTextViewStack()
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
if let textArea = nsView.documentView as? NSTextView, textArea.string != self.text {
textArea.string = self.text
}
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
class Coordinator: NSObject, NSTextViewDelegate {
var text: Binding<String>
init(text: Binding<String>) {
self.text = text
}
func textView(_ textView: NSTextView, shouldChangeTextIn range: NSRange, replacementString text: String?) -> Bool {
defer {
self.text.wrappedValue = (textView.string as NSString).replacingCharacters(in: range, with: text!)
}
return true
}
fileprivate lazy var textStorage = NSTextStorage()
fileprivate lazy var layoutManager = NSLayoutManager()
fileprivate lazy var textContainer = NSTextContainer()
fileprivate lazy var textView: NSTextView = NSTextView(frame: CGRect(), textContainer: textContainer)
fileprivate lazy var scrollview = NSScrollView()
func createTextViewStack() -> NSScrollView {
let contentSize = scrollview.contentSize
textContainer.containerSize = CGSize(width: contentSize.width, height: CGFloat.greatestFiniteMagnitude)
textContainer.widthTracksTextView = true
textView.minSize = CGSize(width: 0, height: 0)
textView.maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
textView.isVerticallyResizable = true
textView.frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
textView.autoresizingMask = [.width]
textView.delegate = self
scrollview.borderType = .noBorder
scrollview.hasVerticalScroller = true
scrollview.documentView = textView
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
return scrollview
}
}
}