0

I have text but it's not fit. I want use marquee when text not fit in my default frame.

Text(self.viewModel.soundTrack.title)
    .font(.custom("Avenir Next Regular", size: 24))
    .multilineTextAlignment(.trailing)
    .lineLimit(1)
    .foregroundColor(.white)
    .fixedSize(horizontal: false, vertical: true)
    //.frame(width: 200.0, height: 30.0)
Sam
  • 2,350
  • 1
  • 11
  • 22
  • Does this answer your question? [The text doesn't get wrapped in swift UI](https://stackoverflow.com/questions/56505929/the-text-doesnt-get-wrapped-in-swift-ui) – LinusGeffarth Mar 21 '20 at 12:58
  • No, I want to do like apple music. One line but text move form one side to another –  Mar 21 '20 at 13:03
  • show us the picture of requested behavior. "I want to do like apple music" has no meaning for me. – user3441734 Mar 21 '20 at 15:00
  • https://i.stack.imgur.com/JdDWX.png –  Mar 21 '20 at 15:10

3 Answers3

6

Try below code....

In MarqueeText.swift

import SwiftUI

struct MarqueeText: View {

    @State private var leftMost = false

    @State private var w: CGFloat = 0

    @State private var previousText: String = ""

    @State private var contentViewWidth: CGFloat = 0

    @State private var animationDuration: Double = 5

    @Binding var text : String

    var body: some View {
        let baseAnimation = Animation.linear(duration: self.animationDuration)//Animation duration
        let repeated = baseAnimation.repeatForever(autoreverses: false)
        return VStack(alignment:.center, spacing: 0) {
            GeometryReader { geometry in//geometry.size.width will provide container/superView width
                Text(self.text).font(.system(size: 24)).lineLimit(1).foregroundColor(.clear).fixedSize(horizontal: true, vertical: false).background(TextGeometry()).onPreferenceChange(WidthPreferenceKey.self, perform: {
                    self.w = $0
                    print("textWidth:\(self.w)")
                    print("geometry:\(geometry.size.width)")
                    self.contentViewWidth = geometry.size.width
                    if self.text.count != self.previousText.count && self.contentViewWidth < self.w {
                        let duration = self.w/50
                        print("duration:\(duration)")
                        self.animationDuration = Double(duration)
                        self.leftMost = true
                    } else {
                        self.animationDuration = 0.0
                    }
                    self.previousText = self.text
                    }).fixedSize(horizontal: false, vertical: true)// This Text is temp, will not be displayed in UI. Used to identify the width of the text.
                if self.animationDuration > 0.0 {
                    Text(self.text).font(.system(size: 24)).lineLimit(nil).foregroundColor(.green).fixedSize(horizontal: true, vertical: false).background(TextGeometry()).onPreferenceChange(WidthPreferenceKey.self, perform: { _ in
                                    if self.text.count != self.previousText.count && self.contentViewWidth < self.w {

                                    } else {
                                        self.leftMost = false
                                    }
                                    self.previousText = self.text
                        }).modifier(self.makeSlidingEffect().ignoredByLayout()).animation(repeated, value: self.leftMost).clipped(antialiased: true).offset(y: -8)//Text with animation
                }
                else {
                    Text(self.text).font(.system(size: 24)).lineLimit(1).foregroundColor(.blue).fixedSize(horizontal: true, vertical: false).background(TextGeometry()).fixedSize(horizontal: false, vertical: true).frame(maxWidth: .infinity, alignment: .center).offset(y: -8)//Text without animation
                }
            }
            }.fixedSize(horizontal: false, vertical: true).layoutPriority(1).frame(maxHeight: 50, alignment: .center).clipped()

    }


    func makeSlidingEffect() -> some GeometryEffect {
      return SlidingEffect(
        xPosition: self.leftMost ? -self.w : self.w,
        yPosition: 0).ignoredByLayout()
    }
}

struct MarqueeText_Previews: PreviewProvider {
    @State static var myCoolText = "myCoolText"
    static var previews: some View {
        MarqueeText(text: $myCoolText)
    }
}

struct SlidingEffect: GeometryEffect {
    var xPosition: CGFloat = 0
    var yPosition: CGFloat = 0

  var animatableData: CGFloat {
    get { return xPosition }
    set { xPosition = newValue }
  }

  func effectValue(size: CGSize) -> ProjectionTransform {
    let pt = CGPoint(
      x: xPosition,
      y: yPosition)
    return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y)).inverted()
  }
}

struct TextGeometry: View {
    var body: some View {
        GeometryReader { geometry in
            return Rectangle().fill(Color.clear).preference(key: WidthPreferenceKey.self, value: geometry.size.width)
        }
    }
}

struct WidthPreferenceKey: PreferenceKey {
    static var defaultValue = CGFloat(0)

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }

    typealias Value = CGFloat
}

struct MagicStuff: ViewModifier {
    func body(content: Content) -> some View {
        Group {
            content.alignmentGuide(.underlineLeading) { d in
                return d[.leading]
            }
        }
    }
}

extension HorizontalAlignment {
    private enum UnderlineLeading: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat {
            return d[.leading]
        }
    }
    static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}

In your existing SwiftUI struct. (The below sample code will check 3 cases 1.Empty string, 2.Short string that doesn't need to marquee, 3.Lengthy marquee string)

@State var value = ""
@State var counter = 0

var body: some View {
    VStack {
        Spacer(minLength: 0)
        Text("Monday").background(Color.yellow)
        HStack {
            Spacer()
            VStack {
                Text("One").background(Color.blue)
            }
            VStack {
            MarqueeText(text: $value).background(Color.red).padding(.horizontal, 8).clipped()
            }
            VStack {
            Text("Two").background(Color.green)
            }
            Spacer()

        }
        Text("Tuesday").background(Color.gray)
        Spacer(minLength: 0)
        Button(action: {
            self.counter = self.counter + 1
            if (self.counter % 2 == 0) {
                self.value = "1Hello World! Hello World! Hello World! Hello World! Hello World!"
            } else {
                self.value = "1Hello World! Hello"
            }
        }) {
            Text("Button")
        }
        Spacer()
    }
}
Shebin Koshy
  • 1,182
  • 8
  • 22
  • I see one problem when change text while app is running your code don't change text –  Mar 22 '20 at 21:48
  • @Vladikug Edited my answer, please try it. – Shebin Koshy Mar 23 '20 at 10:50
  • Now text is change it! But animating disappear. Text is static –  Mar 23 '20 at 15:44
  • @Vladikug if you try the exact same code, on initial launch the text will be empty so there is nothing to display. If you click on the button, the text will change to short text, since short text is fitting inside screen, the animation won’t be there, if you tap on the button again the text will change to lengthy one, so animation will start. – Shebin Koshy Mar 23 '20 at 15:45
  • Yes I know it,. Now is working I miss one thing. Also how can I set then animating is don't need text is align center? Now is left corner –  Mar 23 '20 at 15:56
  • @Vladikug Edited my answer with the requested change – Shebin Koshy Mar 23 '20 at 16:05
  • Amazing! Last question really! How can I use frame for animation I want animation add in frame not from one screen side to another –  Mar 23 '20 at 16:07
  • 2
    @Vladikug Internet only. I have experience in iOS native app development(swift&objectiveC). – Shebin Koshy Mar 23 '20 at 19:26
  • If you use padding(.horizontal, 8).clipped() also you need somewhere in code minus 8 for screen, right? But I can't see where you do this. Because if I want padding(.horizontal, 50).clipped() textWidth change –  Mar 23 '20 at 19:37
  • @Vladikug Without changing any code `MarqueeText(text: $value).background(Color.red).padding(.horizontal, 50).clipped()` working fine for me. – Shebin Koshy Mar 23 '20 at 19:53
  • 1
    I think self.text.count != self.previousText.count this line is not needed, no? –  Mar 24 '20 at 10:40
  • @Vladikug i think, its required – Shebin Koshy Mar 24 '20 at 12:04
  • https://stackoverflow.com/questions/60992321/how-can-detect-watch-os-is-ios-app-closed/60997490#60997490 maybe you can help? –  Apr 02 '20 at 17:43
1

Install https://github.com/SwiftUIKit/Marquee 0.2.0 above with Swift Package Manager and try below code....

struct ContentView: View {
    var body: some View {
        Marquee {
            Text("Hello World!")
                .font(.system(size: 40))
        }
        // This is the key point.
        .marqueeWhenNotFit(true)
    }
}

When you keep increasing the length of the text until it exceeds the width of the marquee, the marquee animation will automatically start.

Catch Zeng
  • 11
  • 3
  • when I use this Marquee in Splash Screen for Linear loader this generates a issue with home Page View. the issue is Menu Bar height automatically larger then I did set in first View when we Action any navigation then it set As I set. – Anup Kumar Mishra Nov 16 '22 at 09:23
1

I was looking for the same thing, but every solution I tried either did not meet my specifications or caused layout/rendering issues, especially when the text changed or the parent view was refreshed. I ended up just writing something from scratch. It is quite hack-y, but it seems to be working now. I would welcome any suggestions on how it can be improved!

import SwiftUI

struct Marquee: View {
    
    @ObservedObject var controller:MarqueeController
    
    var body: some View {
        VStack {
            
            if controller.changing  {
                
                Text("")
                    .font(Font(controller.font))
                
            } else {
                
                if !controller.shouldAnimate {
                    
                    Text(controller.text)
                        .font(Font(controller.font))
                    
                } else {
                    
                    AnimatedText(controller: controller)
                    
                }
            }
            
        }
        .onAppear() {
            
            self.controller.checkForAnimation()
            
        }
        .onReceive(controller.$text) {_ in
            
            self.controller.checkForAnimation()
            
        }
    }
}

struct AnimatedText: View {
    
    @ObservedObject var controller:MarqueeController
    
    var body: some View {
        
        Text(controller.text)
            .font(Font(controller.font))
            .lineLimit(1)
            .fixedSize()
            .offset(x: controller.animate ? controller.initialOffset - controller.offset : controller.initialOffset)
            .frame(width:controller.maxWidth)
            .mask(Rectangle())
        
        
    }
}


class MarqueeController:ObservableObject {
    
    @Published var text:String
    @Published var animate = false
    @Published var changing = true
    @Published var offset:CGFloat = 0
    @Published var initialOffset:CGFloat = 0
    
    var shouldAnimate:Bool {text.widthOfString(usingFont: font) > maxWidth}
    let font:UIFont
    var maxWidth:CGFloat
    var textDoubled = false
    let delay:Double
    let duration:Double
    
    
    init(text:String, maxWidth:CGFloat, font:UIFont = UIFont.systemFont(ofSize: 12), delay:Double = 1, duration:Double = 3) {
        
        self.text = text
        self.maxWidth = maxWidth
        self.font = font
        self.delay = delay
        self.duration = duration
        
        
    }
    
    func checkForAnimation() {
        
        if shouldAnimate  {
            
            let spacer = "    "
            
            if !textDoubled {
                self.text += (spacer + self.text)
                self.textDoubled = true
            }
            
            let textWidth = self.text.widthOfString(usingFont: font)
            
            self.initialOffset = (textWidth - maxWidth) / 2
            
            self.offset = (textWidth + spacer.widthOfString(usingFont: font)) / 2
            
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            
            self.changing = false
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                
                withAnimation(Animation.linear(duration:self.duration).delay(self.delay).repeatForever(autoreverses: false)) {
                    
                    self.animate = self.shouldAnimate
                    
                }
            }
        }
    }
}
kamisama42
  • 595
  • 4
  • 18