How to consistently separate elements on a HStack - swift

I have a set of elements on a HStack that are composed of a circle and some label text underneath it. These elements have a separation between them.
Everything works as expected until one of the labels is bigger than the circle. Then, the text increases the actual width of the element, making the visible separation between the circles, off.
Is there any way to evenly separate the circles, ignoring the text?
struct Item: View {
let color: Color
let label: String
var body: some View {
VStack {
Circle()
.fill(color)
.frame(width: 40, height: 40)
Text(label)
}
}
}
struct ItemSeparation: View {
var body: some View {
HStack(alignment: .top, spacing: 30) {
Item(color: .yellow, label: "Berlin")
Item(color: .green, label: "Copenhagen")
Item(color: .blue, label: "Madrid")
Item(color: .purple, label: "Helsinki")
}
}
}

These two solutions have different compromises. You'll need to decide what happens, for example, if the views will be too wide for the parent view based on the longest label. Option 1 wraps the text. Option 2 makes the view expand beyond its parent.
Option #1:
Use LazyVGrid:
struct ContentView: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
]
var body: some View {
LazyVGrid(columns: columns, spacing: 30) {
Item(color: .yellow, label: "Berlin")
Item(color: .green, label: "Copenhagen")
Item(color: .blue, label: "Madrid")
Item(color: .purple, label: "Helsinki")
}
}
}
Option #2:
Use a PreferenceKey and a GeometryReader to get the size of the widest View and propagate the changes back up to the parent.
struct Item: View {
let color: Color
let label: String
let width: CGFloat
var body: some View {
VStack {
Circle()
.fill(color)
.frame(width: 40, height: 40)
Text(label)
}
.border(Color.blue)
.background(
GeometryReader {
Color.clear.preference(key: ViewWidthKey.self,
value: $0.size.width)
}.scaledToFill()
)
.frame(width: width)
}
}
struct ViewWidthKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
let nextValue = nextValue()
guard nextValue > value else { return }
value = nextValue
}
}
struct ContentView: View {
#State private var maxWidth : CGFloat = 0
var body: some View {
HStack(alignment: .top, spacing: 30) {
Item(color: .yellow, label: "Berlin", width: maxWidth)
Item(color: .green, label: "Copenhagen", width: maxWidth)
Item(color: .blue, label: "Madrid", width: maxWidth)
Item(color: .purple, label: "Helsinki", width: maxWidth)
}.onPreferenceChange(ViewWidthKey.self) { width in
self.maxWidth = width
print(width)
}
}
}

Related

Drawing Rectangles using a ForEach loop SwiftUI

Im trying to create a view of a rectangle with a given amount of rectangles inside of it,
i.e.Image
Im trying to recreate this with the option to pass in a size to add more levels to it, currently I've only managed to overlay
let colors: [Color] = [.red, .green, .blue]
var body: some View {
ZStack {
ForEach(colors, id: \.self) { color in
Text(color.description.capitalized)
.background(color)
}
}
}
which I found only whilst looking for the problem, the only issue I have is having adjustable size values which while using the loop can get progressively smaller, currently the closest I've got is this:
struct SquareContent{
var color: Color
var size: CGSize
}
struct GrannySquareComplete: View {
let colors: [SquareContent] = [
SquareContent.init(color: .red, size: CGSize(width: 100, height: 100)),
SquareContent.init(color: .white, size: CGSize(width: 80, height: 80)),
SquareContent.init(color: .red, size: CGSize(width: 80, height: 80))]
var body: some View {
ZStack {
ForEach(0...colors.count) { i in
Text("")
.frame(width: colors[i].size.width, height: colors[i].size.height)
.background(colors[i].color)
.border(.white)
}
}
}
}
but this returns an error of
No exact matches in call to initialiser
I believe this is due to the use of Text within the foreach loop but cannot figure out how to fix this.
To fix your error change ForEach(0...colors.count) { i in to
ForEach(0..<colors.count) { i in
To make it dynamic
//Make Identifiable
struct SquareContent: Identifiable{
//Add id
let id: UUID = UUID()
var color: Color
//Handle size based on screen
}
struct GrannySquareComplete: View {
//Change to #State
#State var colors: [SquareContent] = [
SquareContent.init(color: .red),
SquareContent.init(color: .white),
SquareContent.init(color: .red)]
var body: some View {
VStack{
//Mimic adding a square
Button("add square"){
colors.append(.init(color: [.blue, .black,.gray, .orange, .pink].randomElement()!))
}
ZStack(alignment: .center){
ForEach(Array(colors.enumerated()), id: \.offset) { (index, color) in
//Calculate the size by percentange based on index
let percentage: CGFloat = Double(colors.count - index)/Double(colors.count)
//Create a custom view to handle the details
SquareComplete(square: color, percentage: percentage)
}
}
}
}
}
struct SquareComplete: View {
let square: SquareContent
let percentage: CGFloat
var body: some View{
GeometryReader{ geo in
Rectangle()
//Make a square
.aspectRatio(1, contentMode: .fit)
.border(.white)
.background(square.color)
.foregroundColor(.clear)
//Center
.position(x: geo.size.width/2, y: geo.size.height/2)
//Adjust size by using the available space and the passed percentage
.frame(width: geo.size.width * percentage, height: geo.size.height * percentage, alignment: .center)
}
}
}
struct GrannySquareComplete_Previews: PreviewProvider {
static var previews: some View {
GrannySquareComplete()
}
}

SwiftUI - Button inside a custom Collection view is not tappable completely

I have a set of buttons to show for the user and I used CollectionView to align the buttons. Each button is a Vstack with an Image and Text components. The tap is reactive only on the image but not on Text and the padding space around.
I am looking to solve this to make it reactive all over the button.
I found suggestions
to set ContentShape to rectangle and it didn't work
use Hstack to insert spaces on Left and right of the Text but that didn't work either.
Sample code:
ToolBarItem:
var body: some View {
VStack {
Button(action: {
// Delegate event to caller/parent view
self.onClickAction(self.toolBarItem)
}) {
VStack {
HStack {
Spacer()
Image(self.toolBarItem.selectedBackgroundImage)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.padding(EdgeInsets(top: 5, leading: 3, bottom: 0, trailing: 3))
.frame(width: CGFloat(self.toolBarItem.cellWidth * 0.60),
height: CGFloat(self.toolBarItem.cellHeight * 0.60))
Spacer()
}
.contentShape(Rectangle())
HStack {
Spacer()
Text(self.toolBarMenuInfo.specialSelectedName)
.foregroundColor(Color.red)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 5, trailing: 0))
Spacer()
}
.contentShape(Rectangle())
}
.frame(width: CGFloat(self.toolBarItem.cellWidth),
height: CGFloat(self.toolBarItem.cellHeight))
.background(Color.blue.opacity(0.5))
}
}
}
The above ToolBarItem is placed inside the Collection view (custom Object created by me) for as many items required. Refer attachment and the tap occurs only on the image surrounded by green marking.
has anyone had similar issue? Any inputs is appreciated.
I strongly suspect that you issue has to do with the border. but I don't know exactly because you haven't provided that code.
Here is a version of the button view that would give you the effect you see to want.
struct FloatingToolbarButtonView: View {
#Binding var model: ButtonModel
let size: CGSize
var body: some View {
Button(action: {
//Set the model's variable to selected
model.isSelected.toggle()
//Perform the action
model.onClick()
}, label: {
VStack {
//REMOVE systemName: in your code
Image(systemName: model.imageName)
//.renderingMode(.original)
.resizable()
//Maintains proportions
.scaledToFit()
//Set Image color
.foregroundColor(.white)
//Works with most images to change color
.colorMultiply(model.colorSettings.imageNormal)
.padding(5)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
//Set border color/width
.border(Color.green, width: model.isSelected ? 3:0)
Spacer()
Text(model.label)
//Set Text color
.foregroundColor(model.colorSettings.labelNormal)
}
.padding(EdgeInsets(top: 5, leading: 3, bottom: 0, trailing: 3))
}).frame(width: size.width, height: size.height, alignment: .center)
.background(model.colorSettings.backgroundNormal)
}
}
And this is what the model I used looks like
//Holds Button information
struct ButtonModel: Identifiable{
let id: UUID = UUID()
var label: String
var imageName: String
///Action to be called when the button is pressed
var onClick: () -> Void
///identify if the user has selected this button
var isSelected: Bool = false
var colorSettings: ButtonColorSettings
}
I created the buttons in a view model so I can have an easy to to set the action and access isSelected as needed.
//ViewModel that deals with all the button creation and onClick actions
class FloatingToolbarParentViewModel: ObservableObject{
//Settings the buttons at this level lets you read `isPressed`
#Published var horizontalButtons: [ButtonModel] = []
#Published var moreButton: [ButtonModel] = []
#Published var verticalButtons: [ButtonModel] = []
init(){
horizontalButtons = horizontalSamples
moreButton = [mer]
verticalButtons = veticalSamples
}
}
//MARK: Buttons
extension FloatingToolbarParentViewModel{
//MARK: SAMPLES fill in with your data
var identify:ButtonModel {ButtonModel(label: "Identify", imageName: "arrow.up.backward", onClick: {print(#function + " Identfy")}, colorSettings: .white)}
var tiltak:ButtonModel {ButtonModel(label: "Tiltak", imageName: "scissors", onClick: {print(#function + " Tiltak")}, colorSettings: .white)}
var tegn:ButtonModel { ButtonModel(label: "Tegn", imageName: "pencil", onClick: {print(#function + " Tegn")}, colorSettings: .white)}
var bestand:ButtonModel {ButtonModel(label: "Bestand", imageName: "leaf", onClick: {print(#function + " Identfy")}, colorSettings: .red)}
var mer:ButtonModel {ButtonModel(label: "Mer", imageName: "ellipsis.circle", onClick: {print(#function + " Mer")}, colorSettings: .red)}
var kart:ButtonModel {ButtonModel(label: "Kart", imageName: "map.fill", onClick: {print(#function + " Kart")}, colorSettings: .white)}
var posisjon:ButtonModel {ButtonModel(label: "Posisjon", imageName: "magnifyingglass", onClick: {print(#function + " Posisjon")}, colorSettings: .white)}
var spor:ButtonModel {ButtonModel(label: "Spor", imageName: "circle.fill", onClick: {print(#function + " Spor")}, colorSettings: .red)}
var horizontalSamples :[ButtonModel] {[identify,tiltak,tegn,bestand]}
var veticalSamples :[ButtonModel] {[kart,posisjon,spor]}
}
The rest of the code to get the sample going is below. It isn't really needed but it will give you a working product
struct FloatingToolbarParentView: View {
#State var region: MKCoordinateRegion = .init()
#StateObject var vm: FloatingToolbarParentViewModel = .init()
var body: some View {
ZStack{
Map(coordinateRegion: $region)
ToolbarOverlayView( horizontalButtons: $vm.horizontalButtons, cornerButton: $vm.moreButton, verticalButtons: $vm.verticalButtons)
}
}
}
struct ToolbarOverlayView: View{
#State var buttonSize: CGSize = .zero
#Binding var horizontalButtons: [ButtonModel]
#Binding var cornerButton: [ButtonModel]
#Binding var verticalButtons: [ButtonModel]
var body: some View{
GeometryReader{ geo in
VStack{
HStack{
Spacer()
VStack{
Spacer()
FloatingToolbarView(buttons: $verticalButtons, buttonSize: buttonSize, direction: .vertical)
}
}
Spacer()
HStack{
Spacer()
FloatingToolbarView(buttons: $horizontalButtons, buttonSize: buttonSize)
FloatingToolbarView(buttons: $cornerButton, buttonSize: buttonSize)
}
//Adjust the button size on appear and when the orientation changes
.onAppear(perform: {
setButtonSize(size: geo.size)
})
.onChange(of: geo.size.width, perform: { new in
setButtonSize(size: geo.size)
})
}
}
}
//Sets the button size using and minimum and maximum values accordingly
//landscape and portrait have oppositive min and max
func setButtonSize(size: CGSize){
buttonSize = CGSize(width: min(size.width, size.height) * 0.15, height: max(size.width, size.height) * 0.1)
}
}
//Toolbar group for an array of butons
struct FloatingToolbarView: View {
#Binding var buttons :[ButtonModel]
let buttonSize: CGSize
var direction: Direction = .horizontal
var body: some View {
Group{
switch direction {
case .horizontal:
HStack(spacing: 0){
ForEach($buttons){$button in
FloatingToolbarButtonView(model: $button, size: buttonSize)
}
}
case .vertical:
VStack(spacing: 0){
ForEach($buttons){$button in
FloatingToolbarButtonView(model: $button, size: buttonSize)
}
}
}
}
}
enum Direction{
case horizontal
case vertical
}
}
#available(iOS 15.0, *)
struct FloatingToolbarParentView_Previews: PreviewProvider {
static var previews: some View {
FloatingToolbarParentView()
FloatingToolbarParentView().previewInterfaceOrientation(.landscapeLeft)
}
}
//Holds Button Color information
//You havent provided much info on this so I assume that you are setting the colors somewhere
struct ButtonColorSettings{
var labelNormal: Color
var imageNormal: Color
var backgroundNormal: Color
//Sample Color configuration per image
static var white = ButtonColorSettings(labelNormal: .white, imageNormal: .white, backgroundNormal: .black.opacity(0.5))
static var red = ButtonColorSettings(labelNormal: .black, imageNormal: .red, backgroundNormal: .white)
}
Have you tried putting .contentShape(Rectangle()) on the whole VStack inside the Button or on the button itself? That should probably solve it.

How Do I get uniform view size when using Image and SFSymbols in SwiftUI?

Here is my View:
I want all boxes (the red rectangles) to be the same size (heights all equal to each other and widths all equal to each other). They don't need to be square.
When I create views using Image(systemname:) they have different intrinsic sizes. How do I make them the same size without hard-coding the size.
struct InconsistentSymbolSizes: View {
let symbols = [ "camera", "comb", "diamond", "checkmark.square"]
var body: some View {
HStack(spacing: 0) {
ForEach(Array(symbols), id: \.self) { item in
VStack {
Image(systemName: item).font(.largeTitle)
}
.padding()
.background(.white)
.border(.red)
}
}
.border(Color.black)
}
}
If you want to normalize the sizes, you could use a PreferenceKey to measure the largest size and make sure that all of the other sizes expand to that:
struct ContentView: View {
let symbols = [ "camera", "comb", "diamond", "checkmark.square"]
#State private var itemSize = CGSize.zero
var body: some View {
HStack(spacing: 0) {
ForEach(Array(symbols), id: \.self) { item in
VStack {
Image(systemName: item).font(.largeTitle)
}
.padding()
.background(GeometryReader {
Color.clear.preference(key: ItemSize.self,
value: $0.frame(in: .local).size)
})
.frame(width: itemSize.width, height: itemSize.height)
.border(.red)
}.onPreferenceChange(ItemSize.self) {
itemSize = $0
}
}
.border(Color.black)
}
}
struct ItemSize: PreferenceKey {
static var defaultValue: CGSize { .zero }
static func reduce(value: inout Value, nextValue: () -> Value) {
let next = nextValue()
value = CGSize(width: max(value.width,next.width),
height: max(value.height,next.height))
}
}

Custom Segmented Controller SwiftUI Frame Issue

I would like to create a custom segmented controller in SwiftUI, and I found one made from this post. After slightly altering the code and putting it into my ContentView, the colored capsule would not fit correctly.
Here is an example of my desired result:
This is the result when I use it in ContentView:
CustomPicker.swift:
struct CustomPicker: View {
#State var selectedIndex = 0
var titles = ["Item #1", "Item #2", "Item #3", "Item #4"]
private var colors = [Color.red, Color.green, Color.blue, Color.purple]
#State private var frames = Array<CGRect>(repeating: .zero, count: 4)
var body: some View {
VStack {
ZStack {
HStack(spacing: 4) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
.foregroundColor(.black)
.font(.system(size: 16, weight: .medium, design: .default))
.bold()
}.padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)).background(
GeometryReader { geo in
Color.clear.onAppear { self.setFrame(index: index, frame: geo.frame(in: .global)) }
}
)
}
}
.background(
Capsule().fill(
self.colors[self.selectedIndex].opacity(0.4))
.frame(width: self.frames[self.selectedIndex].width,
height: self.frames[self.selectedIndex].height, alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
, alignment: .leading
)
}
.animation(.default)
.background(Capsule().stroke(Color.gray, lineWidth: 3))
}
}
func setFrame(index: Int, frame: CGRect) {
self.frames[index] = frame
}
}
ContentView.swift:
struct ContentView: View {
#State var itemsList = [Item]()
func loadData() {
if let url = Bundle.main.url(forResource: "Data", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let jsonData = try decoder.decode(Response.self, from: data)
for post in jsonData.content {
self.itemsList.append(post)
}
} catch {
print("error:\(error)")
}
}
}
var body: some View {
NavigationView {
VStack {
Text("Item picker")
.font(.system(.title))
.bold()
CustomPicker()
Spacer()
ScrollView {
VStack {
ForEach(itemsList) { item in
ItemView(text: item.text, username: item.username)
.padding(.leading)
}
}
}
.frame(height: UIScreen.screenHeight - 224)
}
.onAppear(perform: loadData)
}
}
}
Project file here
The problem with the code as-written is that the GeometryReader value is only sent on onAppear. That means that if any of the views around it change and the view is re-rendered (like when the data is loaded), those frames will be out-of-date.
I solved this by using a PreferenceKey instead, which will run on each render:
struct CustomPicker: View {
#State var selectedIndex = 0
var titles = ["Item #1", "Item #2", "Item #3", "Item #4"]
private var colors = [Color.red, Color.green, Color.blue, Color.purple]
#State private var frames = Array<CGRect>(repeating: .zero, count: 4)
var body: some View {
VStack {
ZStack {
HStack(spacing: 4) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
.foregroundColor(.black)
.font(.system(size: 16, weight: .medium, design: .default))
.bold()
}
.padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16))
.measure() // <-- Here
.onPreferenceChange(FrameKey.self, perform: { value in
self.setFrame(index: index, frame: value) //<-- this will run each time the preference value changes, will will happen any time the frame is updated
})
}
}
.background(
Capsule().fill(
self.colors[self.selectedIndex].opacity(0.4))
.frame(width: self.frames[self.selectedIndex].width,
height: self.frames[self.selectedIndex].height, alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
, alignment: .leading
)
}
.animation(.default)
.background(Capsule().stroke(Color.gray, lineWidth: 3))
}
}
func setFrame(index: Int, frame: CGRect) {
print("Setting frame: \(index): \(frame)")
self.frames[index] = frame
}
}
struct FrameKey : PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
extension View {
func measure() -> some View {
self.background(GeometryReader { geometry in
Color.clear
.preference(key: FrameKey.self, value: geometry.frame(in: .global))
})
}
}
Note that the original .background call was taken out and was replaced with .measure() and .onPreferenceChange -- look for where the //<-- Here note is.
Besides that and the PreferenceKey and View extension, nothing else is changed.

SwiftUI - Sliding Text animation and positioning

On my journey to learn more about SwiftUI, I am still getting confused with positioning my element in a ZStack.
The goal is "simple", I wanna have a text that will slide within a defined area if the text is too long.
Let's say I have an area of 50px and the text takes 100. I want the text to slide from right to left and then left to right.
Currently, my code looks like the following:
struct ContentView: View {
#State private var animateSliding: Bool = false
private let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
private let slideDuration: Double = 3
private let text: String = "Hello, World! My name is Oleg and I would like this text to slide!"
var body: some View {
GeometryReader(content: { geometry in
VStack(content: {
Text("Hello, World! My name is Oleg!")
.font(.system(size: 20))
.id("SlidingText-Animation")
.alignmentGuide(VerticalAlignment.center, computeValue: { $0[VerticalAlignment.center] })
.position(y: geometry.size.height / 2 + self.textSize(fromWidth: geometry.size.width).height / 2)
.fixedSize()
.background(Color.red)
.animation(Animation.easeInOut(duration: 2).repeatForever())
.position(x: self.animateSliding ? -self.textSize(fromWidth: geometry.size.width).width : self.textSize(fromWidth: geometry.size.width).width)
.onAppear(perform: {
self.animateSliding.toggle()
})
})
.background(Color.yellow)
.clipShape(Rectangle())
})
.frame(width: 200, height: 80)
}
func textSize(fromWidth width: CGFloat, fontName: String = "System Font", fontSize: CGFloat = UIFont.systemFontSize) -> CGSize {
let text: UILabel = .init()
text.text = self.text
text.numberOfLines = 0
text.font = UIFont.systemFont(ofSize: 20) // (name: fontName, size: fontSize)
text.lineBreakMode = .byWordWrapping
return text.sizeThatFits(CGSize.init(width: width, height: .infinity))
}
}
Do you have any suggestion how to center the Text in Vertically in its parent and do the animation that starts at the good position?
Thank you for any future help, really appreciated!
EDIT:
I restructured my code, and change a couple things I was doing.
struct SlidingText: View {
let geometryProxy: GeometryProxy
#State private var animateSliding: Bool = false
private let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
private let slideDuration: Double = 3
private let text: String = "Hello, World! My name is Oleg and I would like this text to slide!"
var body: some View {
ZStack(alignment: .leading, content: {
Text(text)
.font(.system(size: 20))
// .lineLimit(1)
.id("SlidingText-Animation")
.fixedSize(horizontal: true, vertical: false)
.background(Color.red)
.animation(Animation.easeInOut(duration: slideDuration).repeatForever())
.offset(x: self.animateSliding ? -textSize().width : textSize().width)
.onAppear(perform: {
self.animateSliding.toggle()
})
})
.frame(width: self.geometryProxy.size.width, height: self.geometryProxy.size.height)
.clipShape(Rectangle())
.background(Color.yellow)
}
func textSize(fontName: String = "System Font", fontSize: CGFloat = 20) -> CGSize {
let text: UILabel = .init()
text.text = self.text
text.numberOfLines = 0
text.font = UIFont(name: fontName, size: fontSize)
text.lineBreakMode = .byWordWrapping
let textSize = text.sizeThatFits(CGSize(width: self.geometryProxy.size.width, height: .infinity))
print(textSize.width)
return textSize
}
}
struct ContentView: View {
var body: some View {
GeometryReader(content: { geometry in
SlidingText(geometryProxy: geometry)
})
.frame(width: 200, height: 40)
}
}
Now the animation looks pretty good, except that I have padding on both right and left which I don't understand why.
Edit2: I also notice by changing the text.font = UIFont.systemFont(ofSize: 20) by text.font = UIFont.systemFont(ofSize: 15) makes the text fits correctly. I don't understand if there is a difference between the system font from SwiftUI or UIKit. It shouldn't ..
Final EDIT with Solution in my case:
struct SlidingText: View {
let geometryProxy: GeometryProxy
#Binding var text: String
#Binding var fontSize: CGFloat
#State private var animateSliding: Bool = false
private let timer = Timer.publish(every: 5, on: .current, in: .common).autoconnect()
private let slideDuration: Double = 3
var body: some View {
ZStack(alignment: .leading, content: {
VStack {
Text(text)
.font(.system(size: self.fontSize))
.background(Color.red)
}
.fixedSize()
.frame(width: geometryProxy.size.width, alignment: animateSliding ? .trailing : .leading)
.clipped()
.animation(Animation.linear(duration: slideDuration))
.onReceive(timer) { _ in
self.animateSliding.toggle()
}
})
.frame(width: self.geometryProxy.size.width, height: self.geometryProxy.size.height)
.clipShape(Rectangle())
.background(Color.yellow)
}
}
struct ContentView: View {
#State var text: String = "Hello, World! My name is Oleg and I would like this text to slide!"
#State var fontSize: CGFloat = 20
var body: some View {
GeometryReader(content: { geometry in
SlidingText(geometryProxy: geometry, text: self.$text, fontSize: self.$fontSize)
})
.frame(width: 400, height: 40)
.padding(0)
}
}
Here's the result visually.
Here is a possible simple approach - the idea is just as simple as to change text alignment in container, anything else can be tuned as usual.
Demo prepared & tested with Xcode 12 / iOS 14
Update: retested with Xcode 13.3 / iOS 15.4
struct DemoSlideText: View {
let text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor"
#State private var go = false
var body: some View {
VStack {
Text(text)
}
.fixedSize()
.frame(width: 300, alignment: go ? .trailing : .leading)
.clipped()
.onAppear { self.go.toggle() }
.animation(Animation.linear(duration: 5).repeatForever(autoreverses: true), value: go)
}
}
One way to do this is by using the "PreferenceKey" protocol, in conjunction with the "preference" and "onPreferenceChange" modifiers.
It's SwiftUI's way to send data from child Views to parent Views.
Code:
struct ContentView: View {
#State private var offset: CGFloat = 0.0
private let text: String = "Hello, World! My name is Oleg and I would like this text to slide!"
var body: some View {
// Capturing the width of the screen
GeometryReader { screenGeo in
ZStack {
Color.yellow
.frame(height: 50)
Text(text)
.fixedSize()
.background(
// Capturing the width of the text
GeometryReader { geo in
Color.red
// Sending width difference to parent View
.preference(key: MyTextPreferenceKey.self, value: screenGeo.size.width - geo.frame(in: .local).width
)
}
)
}
.offset(x: self.offset)
// Receiving width from child view
.onPreferenceChange(MyTextPreferenceKey.self, perform: { width in
withAnimation(Animation.easeInOut(duration: 1).repeatForever()) {
self.offset = width
}
})
}
}
}
// MARK: PreferenceKey
struct MyTextPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0.0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
Result:
Using some inspiration from you guys, I end up doing something way simpler by using only the Animation functions/properties.
import SwiftUI
struct SlidingText: View {
let geometryProxy: GeometryProxy
#Binding var text: String
let font: Font
#State private var animateSliding: Bool = false
private let slideDelay: Double = 3
private let slideDuration: Double = 6
private var isTextLargerThanView: Bool {
if text.size(forWidth: geometryProxy.size.width, andFont: font).width < geometryProxy.size.width {
return false
}
return true
}
var body: some View {
ZStack(alignment: .leading, content: {
VStack(content: {
Text(text)
.font(self.font)
.foregroundColor(.white)
})
.id("SlidingText-Animation")
.fixedSize()
.animation(isTextLargerThanView ? Animation.linear(duration: slideDuration).delay(slideDelay).repeatForever(autoreverses: true) : nil)
.frame(width: geometryProxy.size.width,
alignment: isTextLargerThanView ? (animateSliding ? .trailing : .leading) : .center)
.onAppear(perform: {
self.animateSliding.toggle()
})
})
.clipped()
}
}
And I call the SlidingView
GeometryReader(content: { geometry in
SlidingText(geometryProxy: geometry,
text: self.$playerViewModel.seasonByline,
font: .custom("FoundersGrotesk-RegularRegular", size: 15))
})
.frame(height: 20)
.fixedSize(horizontal: false, vertical: true)
Thank you again for your help!