As the title states, I am looking for a way to wrap multiline text inside of a shape in SwiftUI (see image below). I tried using .clipToShape(MyShape()), but that just renders any text not within the shape invisible.
I have accomplished this in the past using UIKit and exclusion paths, but would like to find a way to achieve the same effect with SwiftUI.
Any advice would be greatly appreciated.
I found a numeric way to do so, but it is probably not the best one.
It should work on IOS and macOS. I tested it on macOS with swiftUI.
The first thing we do is to find out how many rectangles with the hight of the font size are fitting in the circle with its diameter. Then we figure their width out. The last step is grabbing for each rectangle the amount of characters fitting in and adding them into an array. By converting the hole array back to a String we add after each rectangle an "\n" to get the correct multiline alignment.
func createCircularText(text: String, verticalSpacing: Double, circleDiameter: Double, FontSize: Double) -> String {
var Text = text
var circularText = String()
var CountOfWordLines = Int()
var widthOfWordLine = [Int]()
var widthOfWordLineSorted = [Int]()
var array = [String]()
let heigthOfWordLines = FontSize + verticalSpacing
var Dnum = (((1/heigthOfWordLines) * circleDiameter) - 2.0)
Dnum.round(.up)
CountOfWordLines = Int(Dnum)
for n in 1...(CountOfWordLines / 2) {
let num0 = circleDiameter / 2.0
let num1 = pow(num0, 2.0)
let num2 = (Double(n) * heigthOfWordLines)
let num3 = pow(num2,2.0)
let num4 = num1 - num3
let num5 = sqrt(Double(num4))
let num = Int((num5 / 10) * 3)
widthOfWordLine.append(Int(num))
}
widthOfWordLineSorted.append(contentsOf: widthOfWordLine.sorted { $1 > $0 })
widthOfWordLine.removeFirst()
widthOfWordLineSorted.append(contentsOf: widthOfWordLine)
widthOfWordLine.removeAll()
for n in widthOfWordLineSorted {
array.append(String(Text.prefix(n)))
if Text.isEmpty {} else {
let t = Text.dropFirst(n)
Text = String(t)
}
}
circularText = array.joined(separator: "\n")
return circularText
}
In our view we embed the function like this:
#State var text = "your text"
#State var CircularText = String()
// body:
ZStack {
Circle().frame(width: 200)
Text(CircularText)
.multilineTextAlignment(.center)
}
.onAppear(perform: {
CircularText = createCircularText(text: text, verticalSpacing: 3.0, circleDiameter: 200, FontSize: 12)
})
I just tested it with the font size 12, but it should perform with any other as well quite ok.
By changing the diameter you will notice that the text becomes a bit oval, to fix that please change the verticalSpacing. As smaller the number gets, as taller the circle gets, and the other way around. But feel free to fix that issue.
Also, please make sure that your text is long enough.
Related
I am trying to get NSFontPanel/NSFontManager to work in a SwiftUI Document Template app. I have the following which is a customize version of one I found on GitHub. This lets me pick the size, face, style, etc.
Interestingly, a color picker is included in the FontPanel. The documentation doesn't seem to say this. Is this something new?
Anyway, I would like to either be able to use the color picker to let the user select a color, or if not I would like to hide the color picker - at is not "critical" to this application. I am using this to allow customization of text in a sidebar, so color is nice, but not necessary. Currently the Font settings are working, but the color selection displays, and let you pick on, but it always returns System Color.
Any help would be appreciated.
NOTE: I didn't include the FontPickerDelegate, it just calls this:
public struct FontPicker: View{
let labelString: String
#Binding var font: NSFont
#State var fontPickerDelegate: FontPickerDelegate?
public init(_ label: String, selection: Binding<NSFont>) {
self.labelString = label
self._font = selection
}
let fontManager = NSFontManager.shared
let fontPanel = NSFontPanel.shared
#AppStorage("setSidebarFont") var setSidebarFont = "System"
#AppStorage("setSidebarFontSize") var setSidebarFontSize = 24
#AppStorage("setSidebarFontColor") var setSidebarFontColor = "gray"
public var body: some View {
HStack {
Text(labelString)
Button {
if fontPanel.isVisible {
fontPanel.orderOut(nil)
return
}
self.fontPickerDelegate = FontPickerDelegate(self)
fontManager.target = self.fontPickerDelegate
fontManager.action = #selector(fontPickerDelegate?.changeAttributes)
fontPanel.setPanelFont(self.font, isMultiple: false)
fontPanel.orderBack(nil)
} label: {
Text("Font Selection: \(setSidebarFont)")
.font(.custom(setSidebarFont, size: CGFloat(setSidebarFontSize)))
}
}
}
func fontSelected() {
self.font = fontPanel.convert(self.font)
setSidebarFont = self.font.displayName ?? "System"
setSidebarFontSize = Int(self.font.pointSize)
var newAttributes = fontManager.convertAttributes([String : AnyObject]())
newAttributes["NSForegroundColorAttributeName"] = newAttributes["NSColor"]
newAttributes["NSUnderlineStyleAttributeName"] = newAttributes["NSUnderline"]
newAttributes["NSStrikethroughStyleAttributeName"] = newAttributes["NSStrikethrough"]
newAttributes["NSUnderlineColorAttributeName"] = newAttributes["NSUnderlineColor"]
newAttributes["NSStrikethroughColorAttributeName"] = newAttributes["NSStrikethroughColor"]
print("\(newAttributes["NSForegroundColorAttributeName"]!)")
}
}
I was wondering how can one get DragGesture Velocity?
I understand the formula works and how to manually get it but when I do so it is no where what Apple returns (at least some times its very different).
I have the following code snippet
struct SecondView: View {
#State private var lastValue: DragGesture.Value?
private var dragGesture: some Gesture {
DragGesture()
.onChanged { (value) in
self.lastValue = value
}
.onEnded { (value) in
if lastValue = self.lastValue {
let timeDiff = value.time.timeIntervalSince(lastValue.time)
print("Actual \(value)") // <- A
print("Calculated: \((value.translation.height - lastValue.translation.height)/timeDiff)") // <- B
}
}
var body: some View {
Color.red
.frame(width: 50, height: 50)
.gesture(self.dragGesture)
}
}
From above:
A will output something like Value(time: 2001-01-02 16:37:14 +0000, location: (250.0, -111.0), startLocation: (249.66665649414062, 71.0), velocity: SwiftUI._Velocity<__C.CGSize>(valuePerSecond: (163.23212105439427, 71.91841849340494)))
B will output something like Calculated: 287.6736739736197
Note from A I am looking at the 2nd value in valuePerSecond which is the y velocity.
Depending on how you drag, the results will be either different or the same. Apple provides the velocity as a property just like .startLocation and .endLocation but unfortunately there is no way for me to access it (at least none that I know) so I have to calculate it myself, theoretically my calculations are correct but they are very different from Apple. So what is the problem here?
This is another take on extracting the velocity from DragGesture.Value. It’s a bit more robust than parsing the debug description as suggested in the other answer but still has the potential to break.
import SwiftUI
extension DragGesture.Value {
/// The current drag velocity.
///
/// While the velocity value is contained in the value, it is not publicly available and we
/// have to apply tricks to retrieve it. The following code accesses the underlying value via
/// the `Mirror` type.
internal var velocity: CGSize {
let valueMirror = Mirror(reflecting: self)
for valueChild in valueMirror.children {
if valueChild.label == "velocity" {
let velocityMirror = Mirror(reflecting: valueChild.value)
for velocityChild in velocityMirror.children {
if velocityChild.label == "valuePerSecond" {
if let velocity = velocityChild.value as? CGSize {
return velocity
}
}
}
}
}
fatalError("Unable to retrieve velocity from \(Self.self)")
}
}
Just like this:
let sss = "\(value)"
//Intercept string
let start = sss.range(of: "valuePerSecond: (")
let end = sss.range(of: ")))")
let arr = String(sss[(start!.upperBound)..<(end!.lowerBound)]).components(separatedBy: ",")
print(Double(arr.first!)!)
I have a scrolling view that displays an Object's Name as a Text View within a ForEach, I also have a GeometryReader because I need to know the position of each Object within the ScrollView. When displaying the Text View I have a function showObject() set the object's number to be it's position according to the GeometryReader.
So here's my problem. Above the ScrollView I have 3 Texts that show the name and number of the Objects for debug purposes. When scrolling, object[0]'s number updates like I expect it to, but object[1] and object[2] stay at the initialized 100 value. I have a print set up in my showObject() function and it's receiving the correct information, however it appears that it's failing to set the number on my objects after object[0]. Does anyone know why this is happening? Or perhaps a better way of achieving what I'm trying to do?
struct MyObject {
var name:String
var number:CGFloat = 100
}
struct MyScrollView: View {
#State var objects: [MyObject] = [MyObject(name: "a"), MyObject(name: "b"), MyObject(name: "c")]
var body: some View {
VStack{
Text(objects[0].name + " --- " + (objects[0].number.description))
Text(objects[1].name + " --- " + (objects[1].number.description))
Text(objects[2].name + " --- " + (objects[2].number.description))
ScrollView(showsIndicators: false){
VStack{
ForEach(self.options.indices) { i in
GeometryReader {geo in
self.showObject(index: i, num: geo.frame(in: .global).minY)
} // geo
} // ForEach
} // VStack inside ScrollView
} // ScrollView
} // top vstack
} // body
func showObject(index: Int, num: CGFloat) -> Text
{
objects[index].number = num
print (objects[index].name + " -- should be: " + num.description + " -- actual: " + objects[index].number.description)
return Text(objects[index].name)
}
} // view
Any change in #State var objects result in your MyScrollView redraw, ie recreation. So, rendering engine starts drawing your MyScrollView, comes to objects[index].number = num, which modifies #State var objects that reports that new redraw needed...
a -- should be: 94.0 -- actual: 100.0
2019-11-22 08:14:11.535085+0200 Testing[73866:816126] [SwiftUI] Modifying state during view update, this will cause undefined behavior.
... rendering interrupted and goes from start. This is why you observed only 1st MyObject changes.
Solution... well, you need to place .number out of workflow affecting recursive redraw. The simplest is defer modifications to next event cycle, like
if self.objects[index].number != num { // avoid redraw for same values
DispatchQueue.main.async {
self.objects[index].number = num
}
}
However, in general it is better to put MyModel outside of view and make some ViewModel wrapper, which would have view-effecting published name but not number, but this depends on your real needs.
As example I have 3 properties:
var path1FilePath:String = "Src/"
var path2FileName: String = "filename"
var path3Extension: String = ".jpg"
I need to display them with the following way:
HStack {
Text(status.path1FilePath)
Text(status.path2FileName).bold()
Text(status.path3Extension)
}
problem is spacing between Text() views. How to remove them?
SwiftUI allows us to combine strings together like Text("Hello ") + Text("World!"), so you can do the same here:
Text(path1FilePath)
+ Text(path2FileName)
+ Text(path3Extension)
Alternatively, if you still want or need to use an HStack, just use HStack(spacing: 0) and you'll get the same result.
There are 2 ways:
Solution 1:
Text(path1FilePath)
+ Text(path2FileName)
+ Text(path3Extension)
but in this way you cannot apply modifiers =(
Solution 2:
HStack (spacing: 0) {
Text(path1FilePath)
Text(path2FileName)
.bold()
Text(path3Extension)
}
.someTextModifier()
How do I make spacing around NSTextAttachments like in the example below?
In the example No spacing is the default behaviour I get when I append a NSTextAttachment to a NSAttributedString.
This worked for me in Swift
public extension NSMutableAttributedString {
func appendSpacing( points : Float ){
// zeroWidthSpace is 200B
let spacing = NSAttributedString(string: "\u{200B}", attributes:[ NSAttributedString.Key.kern: points])
append(spacing)
}
}
The above answers no longer worked for me under iOS 15. So I ended up creating and adding an empty "padding" attachment in between the image attachment and attributed text
let padding = NSTextAttachment()
//Use a height of 0 and width of the padding you want
padding.bounds = CGRect(width: 5, height: 0)
let attachment = NSTextAttachment(image: image)
let attachString = NSAttributedString(attachment: attachment)
//Insert the padding at the front
myAttributedString.insert(NSAttributedString(attachment: padding), at: 0)
//Insert the image before the padding
myAttributedString.insert(attachString, at: 0)
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] init];
// append NSTextAttachments instance to attributedText...
// then add non-printable string with NSKernAttributeName attributes
unichar c[] = { NSAttachmentCharacter };
NSString *nonprintableString = [NSString stringWithCharacters:c length:1];
NSAttributedString *spacing = [[NSAttributedString alloc] initWithString:nonprintableString attributes:#{
NSKernAttributeName : #(4) // spacing in points
}];
[attributedText appendAttributedString:spacing];
// finally add other text...
Add spacing to image(For iOS 15).
extension UIImage {
func imageWithSpacing(insets: UIEdgeInsets) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(
CGSize(width: self.size.width + insets.left + insets.right,
height: self.size.height + insets.top + insets.bottom), false, self.scale)
UIGraphicsGetCurrentContext()
let origin = CGPoint(x: insets.left, y: insets.top)
self.draw(at: origin)
let imageWithInsets = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return imageWithInsets
}
}
This extension makes it easy to add the space you need. It also works on iOS 16.
extension NSMutableAttributedString {
func appendSpace(_ width: Double) {
let space = NSTextAttachment()
space.bounds = CGRect(x:0, y: 0, width: width, height: 0)
append(NSAttributedString(attachment: space))
}
}
// usages
let example = NSMutableAttributedString()
example.appendSpace(42)
...