Home > Enterprise >  Marquee with buttons inside a view SwiftUI
Marquee with buttons inside a view SwiftUI

Time:10-26

I'm trying to have a marquee horizontal scrolling effect but my buttons are not clickable. The view renders well, but when I tap the buttons it should print out 'tapped user1', for example but there is no effect.

EDIT: If I put the marquee modifier on the ScrollView as suggested, it causes the extremely buggy scrolling behavior. Ideally, this would just be a marquee'd HStack with a bunch of clickable buttons in it with no scrolling behavior built in, but the module doesn't seem to work without the ScrollView wrapping it.

I used this link to create a Marquee view modifier: https://swiftuirecipes.com/blog/swiftui-marquee

My code for the view is below:

struct MyView: View {
    var body: some View {
        let users = ["user1", "user2", "user3", "user4", "user5", "user6"]
        return ScrollView(.horizontal, showsIndicators: false) {
             HStack {
                 ForEach(users, id: \.self) { user in
                     if user == users.first {
                         Button(action: {
                             print("tapped \(user)")
                         }, label: {
                             Text(user)
                         })
                     } else {
                         Text("•")
                         Button(action: {
                             print("tapped \(user)")
                         }, label: {
                             Text(user)
                         })
                     }
                 }
             }.frame(height: 20)
              .marquee(duration: 10)
         }
     }
 }

The code from the marquee tutorial is below:

struct Marquee: ViewModifier {
  let duration: TimeInterval
  let direction: Direction
  let autoreverse: Bool

  @State private var offset = CGFloat.zero
  @State private var parentSize = CGSize.zero
  @State private var contentSize = CGSize.zero

  func body(content: Content) -> some View {
    // measures parent view width
    Color.clear
      .frame(height: 0)
      // measureSize from https://swiftuirecipes.com/blog/getting-size-of-a-view-in-swiftui
      .measureSize { size in
        parentSize = size
        updateAnimation(sizeChanged: true)
      }

    content
      .measureSize { size in
        contentSize = size
        updateAnimation(sizeChanged: true)
      }
      .offset(x: offset)
      // animationObserver from https://swiftuirecipes.com/blog/swiftui-animation-observer
      .animationObserver(for: offset, onComplete: {
        updateAnimation(sizeChanged: false)
      })
  }

  private func updateAnimation(sizeChanged: Bool) {
    if sizeChanged || !autoreverse {
      offset = max(parentSize.width, contentSize.width) * ((direction == .leftToRight) ? -1 : 1)
    }
    withAnimation(.linear(duration: duration)) {
      offset = -offset
    }
  }

  enum Direction {
    case leftToRight, rightToLeft
  }
}

extension View {
  func marquee(duration: TimeInterval,
               direction: Marquee.Direction = .rightToLeft,
               autoreverse: Bool = false) -> some View {
    self.modifier(Marquee(duration: duration,
                          direction: direction,
                          autoreverse: autoreverse))
  }
}

struct SizePreferenceKey: PreferenceKey {
  static var defaultValue: CGSize = .zero

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

struct MeasureSizeModifier: ViewModifier {
  func body(content: Content) -> some View {
    content.background(GeometryReader { geometry in
      Color.clear.preference(key: SizePreferenceKey.self,
                             value: geometry.size)
    })
  }
}

extension View {
  func measureSize(perform action: @escaping (CGSize) -> Void) -> some View {
    self.modifier(MeasureSizeModifier())
      .onPreferenceChange(SizePreferenceKey.self, perform: action)
  }
}

public struct AnimationObserverModifier<Value: VectorArithmetic>: AnimatableModifier {
  // this is the view property that drives the animation - offset, opacity, etc.
  private let observedValue: Value
  private let onChange: ((Value) -> Void)?
  private let onComplete: (() -> Void)?

  // SwiftUI implicity sets this value as the animation progresses
  public var animatableData: Value {
    didSet {
      notifyProgress()
    }
  }

  public init(for observedValue: Value,
              onChange: ((Value) -> Void)?,
              onComplete: (() -> Void)?) {
    self.observedValue = observedValue
    self.onChange = onChange
    self.onComplete = onComplete
    animatableData = observedValue
  }

  public func body(content: Content) -> some View {
    content
  }

  private func notifyProgress() {
    DispatchQueue.main.async {
      onChange?(animatableData)
      if animatableData == observedValue {
        onComplete?()
      }
    }
  }
}

public extension View {
    func animationObserver<Value: VectorArithmetic>(for value: Value,
                                                    onChange: ((Value) -> Void)? = nil,
                                                    onComplete: (() -> Void)? = nil) -> some View {
      self.modifier(AnimationObserverModifier(for: value,
                                                 onChange: onChange,
                                                 onComplete: onComplete))
    }
}

CodePudding user response:

Put the modifier on scrollview, it will fix your issue

Like this.

import SwiftUI
struct MyView: View {
    var body: some View {
        let users = ["user1", "user2", "user3", "user4", "user5", "user6"]
        return ScrollView(.horizontal, showsIndicators: false) {
             HStack {
                 ForEach(users, id: \.self) { user in
                     if user == users.first {
                         Button(action: {
                             print("tapped \(user)")
                         }, label: {
                             Text(user)
                         })
                     } else {
                         Text("•")
                         Button(action: {
                             print("tapped \(user)")
                         }, label: {
                             Text(user)
                         })
                     }
                 }
             }.frame(height: 20)
             
         }
        .marquee(duration: 10)
     }
 }

CodePudding user response:

Try adding the modifier on the button .buttonStyle(PlainButtonStyle()) or similar with other button styles. If that doesn't work, then try using just the button label that you desire and add the modifier .onTapGesture {your code}

  • Related