관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swiftUI] ButtonStyle을 이용한 커스텀 버튼 구현 방법 (ButtonStyle, PrimitiveButtonStyle) 본문

iOS 기본 (SwiftUI)

[iOS - swiftUI] ButtonStyle을 이용한 커스텀 버튼 구현 방법 (ButtonStyle, PrimitiveButtonStyle)

jake-kim 2022. 8. 6. 23:09

목차) SwiftUI의 기본 - 목차 링크

* Button 기본 개념은 이전 포스팅 글 참고

커스텀 버튼 구현 아이디어

커스텀 버튼 3가지

  • ButtonStyle을 준수하는 struct형을 만들고, makeBody(configuration:) 메소드를 구현
    • SwiftUI에서는 상속사용을 지양하기 때문에, 사용하는 쪽에서 .buttonStyle()으로 사용
// 커스텀 ScaleButton을 만들기 위해서 ButtonStyle 준수하고 makeBody 메서드 구현

struct ScaleButton: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    <#code#>
  }
}
  • makeBody 인자 configuration은 label 프로퍼티에 접근할수 있는데, 이 label 인스턴스를 가지고 커스텀
    • isPressed 속성도 존재하여, 버튼 탭 애니메이션도 쉽게 적용이 가능
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public struct ButtonStyleConfiguration {
  /// A type-erased label of a button.
  public struct Label : View {
    public typealias Body = Never
  }
  @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
  public let role: ButtonRole?
  
  /// A view that describes the effect of pressing the button.
  public let label: ButtonStyleConfiguration.Label
  
  /// A Boolean that indicates whether the user is currently pressing the button
  public let isPressed: Bool
}

EdgeInsets Extension만들기

  • 커스텀 UI를 만들기 전에 EdgeInsets을 쉽게 사용하기위해 EdgeInsets 확장
extension EdgeInsets {
  var horizontalInsets: CGFloat { self.trailing + self.leading }
  var verticalInsets: CGFloat { self.top + self.bottom }
  var left: CGFloat { self.leading }
  var right: CGFloat { self.trailing }
  
  static func with(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) -> EdgeInsets {
    EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
  }
  
  static func horizontal(_ horizontal: CGFloat, top: CGFloat = 0, bottom: CGFloat = 0) -> UIEdgeInsets {
    UIEdgeInsets(
      top: top,
      left: horizontal,
      bottom: bottom,
      right: horizontal
    )
  }
  
  static func vertical(_ vertical: CGFloat, left: CGFloat = 0, right: CGFloat = 0) -> UIEdgeInsets {
    UIEdgeInsets(
      top: vertical,
      left: left,
      bottom: vertical,
      right: right
    )
  }
  
  init(_ all: CGFloat) {
    self = EdgeInsets(top: all, leading: all, bottom: all, trailing: all)
  }
  
  init(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) {
    self = EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
  }
  
  init(horizontal: CGFloat = 0, vertical: CGFloat = 0) {
    self = EdgeInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal)
  }
  
  func with(left: CGFloat? = nil, right: CGFloat? = nil, top: CGFloat? = nil, bottom: CGFloat? = nil) -> UIEdgeInsets {
    UIEdgeInsets(
      top: top ?? self.top,
      left: left ?? self.left,
      bottom: bottom ?? self.bottom,
      right: right ?? self.right
    )
  }
  
  func with(horizontal: CGFloat, top: CGFloat? = nil, bottom: CGFloat? = nil) -> UIEdgeInsets {
    UIEdgeInsets(
      top: top ?? self.top,
      left: horizontal,
      bottom: bottom ?? self.bottom,
      right: horizontal
    )
  }
  
  func with(vertical: CGFloat, left: CGFloat? = nil, right: CGFloat? = nil) -> UIEdgeInsets {
    UIEdgeInsets(
      top: vertical,
      left: left ?? self.left,
      bottom: vertical,
      right: right ?? self.right
    )
  }
}

InsetButton

  • label과 button사이에 padding이 존재하는 버튼

  • ButtonStyle 프로토콜을 준수하고, makeBoy안에서 label인스턴스에 접근하여 padding(EdgeInsets())로 inset값 설정
    • 프로퍼티를 선언하면 사용하는쪽에서 초기화할때 주입이 가능
struct InsetButton: ButtonStyle {
  var labelColor = Color.white
  var backgroundColor = Color.blue
  
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .foregroundColor(labelColor)
      .padding(.init(horizontal: 20, vertical: 13))
      .background(backgroundColor)
  }
}
  • 사용하는쪽
Button("InsetButton") {
  print("tap button")
}
.buttonStyle(InsetButton(labelColor: .white, backgroundColor: .blue))

InsetRoundButton

  • Inset이 존재하고 Round 모양의 버튼

  • ButtonStyle을 준수하고, makeBody 안에서 뒤에서 배울 Capsule()을 사용하여 background 구현
struct InsetRoundButton: ButtonStyle {
  var labelColor = Color.white
  var backgroundColor = Color.blue
  
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .foregroundColor(labelColor)
      .padding(.init(horizontal: 20, vertical: 13))
      .background(Capsule().fill(backgroundColor)) // <-
  }
}
  • 사용하는 쪽
Button("InsetRoundButton") {
  print("tap button")
}
.buttonStyle(InsetRoundButton(labelColor: .white, backgroundColor: .blue))

InsetRoundScaleButton

  • Inset이 존재하고 Round 모양이고 눌렀을때 누른 애니메이션을 주기 위해서 축소하는 모션이 있는 버튼

  • scaleEffect 프로퍼티를 사용하여 애니메이션 효과 부여
struct InsetRoundScaleButton: ButtonStyle {
  var labelColor = Color.white
  var backgroundColor = Color.blue
  
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .foregroundColor(labelColor)
      .padding(.init(horizontal: 20, vertical: 13))
      .background(Capsule().fill(backgroundColor))
      .scaleEffect(configuration.isPressed ? 0.88 : 1.0) // <-
  }
}

PrimitiveButtonStyle

https://developer.apple.com/documentation/swiftui/primitivebuttonstyle

  • 더욱 상세하게 커스텀할 수 있는 프로토콜 (인터렉션, appearance 등)
  • PrimitiveButtonStyle로 action의 트리거 타이밍도 직접 커스텀이 가능

ex) long press 조건 심기

* 3 distance만큼 벗어날 경우 long press 실패

3만큼 벗어나서 long press 실패

* 2초 이상 누르고 있어야 long press로 인식

  • PrimitiveButtonStyle 프로토콜을 준수하여 구현
    • makeBody도 구현
struct MyPrimitiveButtonStyle: PrimitiveButtonStyle {
  func makeBody(configuration: PrimitiveButtonStyle.Configuration) -> some View
  }
}
  • makeBody에 리턴할 MyBtton을 내부적으로 구현
    • configuration.trigger()이것을 실행하는 타이밍에 action이 실행
  func makeBody(configuration: PrimitiveButtonStyle.Configuration) -> some View {
    MyButton(configuration: configuration, labelColor: labelColor, backgroundColor: backgroundColor)
  }
  
  struct MyButton: View {
    @GestureState private var pressed = false
    
    let configuration: PrimitiveButtonStyle.Configuration
    var labelColor: Color
    var backgroundColor: Color
    
    var body: some View {
      configuration.label
        .foregroundColor(labelColor)
        .padding(.init(horizontal: 20, vertical: 13))
        .background(Capsule().fill(backgroundColor))
        .scaleEffect(pressed ? 0.88 : 1.0)
        .gesture(
          // minimumDuration - 몇초 이상 지속되어야 longPress로 볼지 결정
          // maximumDistance - 해당 거리보다 더 움직이면 longPress 실패로 결정
          LongPressGesture(minimumDuration: 2, maximumDistance: 3.0)
            .updating($pressed) { value, state, _ in state = value }
            .onEnded { _ in self.configuration.trigger() }
        )
    }

* 참고

https://developer.apple.com/documentation/swiftui/primitivebuttonstyle

https://swiftui-lab.com/custom-styling/

Comments