SwiftUI marquee sample - swiftui

I am trying to find swiftUI code implementing marquee which shows only the rolling text content inside the text view. Text rolling outside to the left and right of the text view will not be shown.

Put the text in an overlay of your box and animate the .offset to scroll the text. Add .clipped() to the box to clip any text that appears outside of the box:
struct ContentView: View {
#State private var textoffset = 300.0
let text = "This is a test of scrolling text. This is only a test."
var body: some View {
Color.yellow
.frame(width: 250, height: 100)
.overlay (
Text(text)
.fixedSize()
.offset(x: textoffset, y: 0)
)
.animation(.linear(duration: 10)
.repeatForever(autoreverses: false), value: textoffset)
.clipped()
.onAppear {
textoffset = -300.0
}
}
}

Related

Have ScrollView always scrolled to bottom

I have a fixed height ScrollView containing a Text() which is constantly being updated from my viewModel.
If the text is too much to be viewed all at once, i.e. I need to scroll to see the end of the text, I’d like it to be automatically scrolled so that I always see the end of the text.
Is that possible?
ScrollView {
Text(vm.text)
.frame(minWidth: 20, alignment: .leading)
}
.frame(height: 200)
Note: this is a very simplified version of my problem. In my app there are times when the text is not being updated and it does need to be scrollable.
I have tried scrollViewReader … something like:
ScrollView {
ScrollViewReader() { proxy in
Text(vm.text)
.frame(minWidth: 20, alignment: .leading)
Text("").id(0)
}
}
.frame(height: 200)
with the idea of scrolling to the empty Text, but I couldn’t work out how to trigger
withAnimation {
proxy.scrollTo(0)
}
... all the examples I've seen use a button but I need to trigger when the text updates.
You can use .onChange to react to change of vm.text and then scroll to the end.
Please note that ScrollView should be inside the ScrollViewReader!
In principle it would work like this:
struct ContentView: View {
#State private var vmtext = "Test Text"
// timer change of text for testing
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Text(vmtext)
.id(0)
}
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
proxy.scrollTo(0, anchor: .bottom)
}
}
.frame(width: 100, height: 200, alignment: .leading)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
vmtext += " added new text"
}
}
}
The problem a have here is that it only works after the scrollview has been scrolled manually once.
I've adapted the code by #ChrisR with a hack that at the very least, shows how i'd like scrollTo: to work. It toggles between 2 almost identical views with different ids (one has a tiny bit of padding)
import SwiftUI
struct ContentView: View {
#State private var vmtext = "\n\n\n\nTest Text"
#State private var number = 0
#State private var id = 0
// timer change of text for testing
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
id == 0 ?
VStack {
Text(vmtext)
.padding(.top, 0)
.font(.title2)
.id(0)
}
:
VStack {
Text(vmtext)
.padding(.top, 1)
.font(.title2)
.id(1)
}
}
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
print("onChange entered. id: \(id)")
proxy.scrollTo(id, anchor: .bottom)
}
}
.frame(height: 90, alignment: .center)
.frame(maxWidth: .infinity)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
if id == 0 { id = 1} else {id = 0}
number += 1
vmtext += " word\(number)"
}
}
}
Notes:
It seems that if the height of the text view being shown hasn't changed the proxy.scrollTo is ignored. Set the paddings the same and the hack breaks.
Removing the "\n\n\n\n" from the var vmtext breaks the hack. They make the initial size of the text view bigger than the scrollview window and so immediately scrollable - or something :-). If you do remove them, scrolling will start working after you do an initial scroll with your finger.
EDIT:
Here is a version without the padding and "\n\n\n\n" hacks, which uses a double rotation hack.
import SwiftUI
struct ContentView: View {
#State private var vmtext = "Test Text"
#State private var number = 0
#State private var id = 0
// timer change of text for testing
let timer = Timer.publish(every: 0.3, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Group {
id == 0 ?
VStack {
Text(vmtext)
.font(.title2)
.id(0)
}
:
VStack {
Text(vmtext)
.font(.title2)
.id(1)
}
}
.padding()
.rotationEffect(Angle(degrees: 180))
}
.rotationEffect(Angle(degrees: 180))
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
withAnimation {
if id == 0 { id = 1 } else { id = 0 }
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame(height: 180, alignment: .center)
.frame(maxWidth: .infinity)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
number += 1
vmtext += " word\(number)"
}
}
}
It would be nice if the text started at the top of the view and only scrolls when the text has filled up the view, as with the swiftui TextEditor ... and the withAnimation doesn't work.

Is there a way to "activate" word-wrapping in SwiftUI?

Is there an alternative to UIKit's LineBreakMode in SwiftUI?
Or are there any alternatives to get the Text truncated via words not via characters in SwiftUI?
I've tried the
.fixedSize(horizontal: false, vertical: true)
approach but it does not seem to work.
Update with the code:
VStack {
HStack {
CustomText("Some Text")
Spacer()
SomeOtherView()
}.padding(.horizontal, 16)
}
Update:
struct CustomText: View {
// MARK: - Properties
let text: String
let color: Color?
// MARK: - Init
init(text: String, color: Color? = Colors.darkGrey) {
self.text = text
self.color = color
}
// MARK: - Body
var body: some View {
Text(text)
.tracking(0.256)
.fontWithLineHeight(font: Fonts.header2, lineHeight: 33)
.foregroundColor(color)
.minimumScaleFactor(0.5)
.lineLimit(nil)
}
}
Some other view is just a Lottie Animation View so it should not make any difference.
An observation is that SwiftUI prefers to add hyphenation rather than minimising the font => I think that the priority of hyphenation is bigger than the minimumScaleProperty.

SwiftUI Frame width issue [duplicate]

I would like to underline a title with a rectangle that should have the same width as the Text.
First I create an underlined text as below:
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}
}
}
So I get the following result:
Now I want to get this result:
So I would like to know if it's possible to bind Text width and apply it to Rectangle by writing something like :
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(width: Text.width, height: (5.0))
}
}
}
By doing so, I could change text and it will be dynamically underlined with correct width.
I tried many options but I can't find how to do it. I also checked this question but it's seems to not be the same issue.
Just specify that container has fixed size and it will tight to content, like
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}.fixedSize() // << here !!
}

Ignore horizontal safe area with vertical ScrollView

I am trying to create a layout with multiple horizontal ScrollViews inside a vertical ScrollView, similar to the template picker in Apple's Pages app. I would like the content of the horizontal ScrollViews to be visible beyond the safe area. However I seem to be unable to get the content of the vertical ScrollView outside the horizontal safe insets. This is visible when iPhones with a notch are used in landscape orientation.
I have tried adding negative padding to the content of the vertical ScrollView. This kind of works, but creates issues when using the device in portrait mode.
Below example code shows the issue. I would expect the rectangles to be visible in beyond the safe area when scrolling horizontally, but they get clipped. How can I make them visible beyond the safe area?
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView(.vertical) {
ScrollView(.horizontal) {
HStack {
Rectangle()
.frame(width: 200, height: 300)
Rectangle()
.frame(width: 200, height: 300)
Rectangle()
.frame(width: 200, height: 300)
}
} .edgesIgnoringSafeArea(.horizontal)
} .edgesIgnoringSafeArea(.horizontal)
}
}
You can detect when the device's orientation changes and adapt your view:
struct ContentView: View {
#Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
Group {
if verticalSizeClass == .compact {
content.edgesIgnoringSafeArea(.horizontal)
} else {
content
}
}
}
var content: some View {
ScrollView(.vertical) {
ScrollView(.horizontal) {
HStack {
Rectangle()
.frame(width: 200, height: 300)
Rectangle()
.frame(width: 200, height: 300)
Rectangle()
.frame(width: 200, height: 300)
}
}
}
.edgesIgnoringSafeArea(.horizontal)
}
}

SwuftUI Gesture location detect clicks out of View border

If I put several Views in a row with no spaces (or very little space between them), and attach some Gesture action, I can't correctly detect, which one is tapped. It looks like there is some padding inside gesture that I can't remove.
here's the example code:
struct ContentView: View {
#State var tap = 0
#State var lastClick = CGPoint.zero
var body: some View {
VStack{
Text("last tap: \(tap)")
Text("coordinates: (x: \(Int(lastClick.x)), y: \(Int(lastClick.y)))")
HStack(spacing: 0){
ForEach(0...4, id: \.self){ind in
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.gray)
.overlay(Text("\(ind)"))
.overlay(Circle()
.frame(width: 4, height: 4)
.foregroundColor(self.tap == ind ? Color.red : Color.clear)
.position(self.lastClick)
)
.frame(width: 40, height: 50)
//.border(Color.black, width: 0.5)
.gesture(DragGesture(minimumDistance: 0)
.onEnded(){value in
self.tap = ind
self.lastClick = value.startLocation
}
)
}
}
}
}
}
and behavior:
I want Gesture to detect 0 button clicked when click location goes negative. Is there a way to do that?
I spent few hours with that problem, and just after I post this question, I found solution.
it was so simple - just to add a contentShape modifier
...
.frame(width: 40, height: 50)
.contentShape(Rectangle())
.gesture(DragGesture(minimumDistance: 0)
...