ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SwiftUI - trim & mask
    SwiftUI 2023. 12. 21. 19:20

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 SwiftUI의 trim과 mask에 대해 학습해보려고 합니다 🙋🏻

     

    우선 이 두개의 모디파이어를 학습해보려는 이유는 간단했어요!

    도형이나 이미지를 잘라서 표시하거나 진행 상황에 따라 가려진 부분을 점진적으로 채워나가려는 구현을 알아보다가 이 두가지를 찾았습니다.

     

    trim과 mask의 결과뷰는 비슷해보이지만 동작하는 방식에서 차이가 있기에 우선 둘 다 어떻게 사용하는것인지 알아보시죠!

     

    먼저 trim부터 알아보겠습니다.


    trim(from:to:)

    trim 메서드는 도형의 특정 부분을 잘라내 보여주는 역할을 해줍니다.

    해당 메서드는 Shape 프로토콜을 준수하는 모든 도형에 사용할 수 있어요!

     

    trim 메서드의 정의는 이렇습니다.

     

    func trim(
        from startFraction: CGFloat = 0,
        to endFraction: CGFloat = 1
    ) -> some Shape

     

    보시면 from과 to 두개의 매개변수를 받아요.

    from은 도형을 그리는 시작점 즉 해당 값 이전에는 도형을 잘라내며 해당 값을 기준으로 그려질 시작점의 값을 의미합니다.

    to는 도형을 그리는 끝점의 값을 의미합니다.

     

    즉, from과 to 값을 적절히 넣어서 어디서부터 어디까지 잘라내고 그릴것인지 사용할 수 있습니다.

    이 매개변수들은 0부터 1사이의 값을 가지며 도형 전체 길이에 대한 비율로 나타냅니다.

     

    예시로, 아래같은 코드가 있다고 가정해볼께요.

     

    Circle()
      .trim(from: 0, to: 0.5)
      .stroke(Color.blue, lineWidth: 20)

     

    원을 그리는데 trim을 사용하고 from이 0 그리고 to가 0.5이니 어떻게 그려질까요?

     

    도형은 이렇게 파란 테두리로 표현된 반원으로 그려지게 됩니다.

     

     

    또한 공식문서에서는 이런 도형의 활용에서도 trim을 보여줍니다.

     

    import SwiftUI
    
    struct ContentView: View {
      var body: some View {
        Path { path in
          path.addLines([
            .init(x: 2, y: 1),
            .init(x: 1, y: 0),
            .init(x: 0, y: 1),
            .init(x: 1, y: 2),
            .init(x: 3, y: 0),
            .init(x: 4, y: 1),
            .init(x: 3, y: 2),
            .init(x: 2, y: 1)
          ])
        }
        .scale(50, anchor: .topLeading)
        .stroke(Color.black, lineWidth: 3)
      }
    }

     

    해당 Path로 선을 그린 코드가 있습니다.

    이 코드는 아래와 같이 마름모 두개가 붙은 도형으로 그려져요.

     

     

    그런데 여기서 첫번째 좌측 마름모의 윗 부분 선 두개만 잘라내고 싶다면 어떻게 trim을 사용할 수 있을까요?

     

    이렇게 사용합니다.

     

    import SwiftUI
    
    struct ContentView: View {
      var body: some View {
        Path { path in
            path.addLines([
                .init(x: 2, y: 1),
                .init(x: 1, y: 0),
                .init(x: 0, y: 1),
                .init(x: 1, y: 2),
                .init(x: 3, y: 0),
                .init(x: 4, y: 1),
                .init(x: 3, y: 2),
                .init(x: 2, y: 1)
            ])
        }
        // 🙋🏻🙋🏻🙋🏻🙋🏻trim🙋🏻🙋🏻🙋🏻🙋🏻
        .trim(from: 0.25, to: 1)
        .scale(50, anchor: .topLeading)
        .stroke(Color.black, lineWidth: 3)
      }
    }

     

    해당 Path를 그릴때 경로들을 비율로 나타내면 좌측 마름모의 선 두개는 전체 그리기 과정 중 25%를 차지합니다.

    그렇기에 0~1의 비율로 환산하면 0.25의 값이 from이 될 수 있습니다.

    그리고 끝까지 그릴테니 to를 1로 두면 원본의 3/4만 나타내게되는 도형을 잘라내어 표시할 수 있습니다.

     

     

     

    요런식으로 말이죠!

     

    이렇게 trim은 도형에서 잘 활용할 수 있습니다.

    주로, 도형을 다 그리지 않고 어떤 부분은 잘라내고 나머지 부분을 표시할때 사용되죠.

     

    우리가 흔히 접할 수 있는, 진행률에 따라 원이 채워지는 프로그레스바의 구현도 이 trim을 이용 할 수 있겠죠? 😁

     

    그런데 trim을 적용할 수 없는것이 있습니다.

    저는 사실 이 학습을 해본것이 이미지에 대해 가리거나 잘라내어 원하는 부분만 표시하고 진척률에 따라 점차 채워가는 구현을 알아보다 여기까지 온거에요 😅

     

    아쉽게도 trim은 Shape 프로토콜을 준수해야지 사용 가능합니다.

    즉 도형에 사용 가능하죠.

    그렇기에 SwiftUI의 Image는 Shape 프로토콜을 준수하지 않기에 trim 메서드를 직접 적용할 수 없습니다.

     

    Image의 일부만을 보여주고 싶은 저의 구현을 완성 시키려면 오늘 배워볼 mask나 clipped, overlay 등의 메서드를 이용해야 하죠!

     

    그래서 바로 이어서 다음 알아볼것이 mask입니다.

     

    clipped나 overlay는 예전 포스팅에서 다룬적이 있기에 안다뤄본 mask를 알아보고 이용해 원하는 구현을 해보겠습니다 🙋🏻


    mask(alignment:_)

    mask 메서드는 한 뷰의 모양을 다른 뷰의 모양으로 제한하는데 사용합니다.

    즉, 원래의 뷰의 일부분만을 보여줄 수 있죠.

     

    흔히 우리 마스킹 테이프, 마스킹 처리 이런 용어들 많이 접했잖아요?


    무언가를 가려서 보여줄때 사용하죠.

     

    그렇기에 mask 메서드도 원래의 뷰에 다른 뷰를 전달하여 얹어서 전달된 뷰가 불투명하면 원본 뷰가 보이지 않고 투명하면 보이는 방식이죠.

    즉, 전달된 뷰의 알파 값을 이용합니다.

     

    정의는 이렇습니다!

     

    func mask<Mask>(
        alignment: Alignment = .center,
        @ViewBuilder _ mask: () -> Mask
    ) -> some View where Mask : View

     

    여기서 alignment 매개변수는 기본적으로 center 즉, 가운데 정렬이고 해당 값을 조정하여 마스킹으로 전달된 뷰의 정렬 방식을 조정할 수 있습니다.

     

    그리고, mask 매개변수로 알파 렌더링 시스템이 지정된 뷰에 적용되는 뷰를 넣을 수 있습니다.

     

    즉, mask는 뷰를 올려서 가려주고 알파 값에 따라 투명도를 조정해 원본 뷰를 보여주는 방식이기에 아예 원본 뷰를 잘라내는 clipped와 같은 메서드와는 다르죠.

     

    예시를 한번 간단히 볼까요?

     

    우선 사용할 이미지는 이런 이미지에요!

     

     

    무지개 애플 로고를 우리는 이번 학습에서 활용할거에요.

    그렇기에 원본 이미지를 잘 기억하고 어떻게 가려지고 처리되는지 알아볼께요!

     

    import SwiftUI
    
    struct ContentView: View {
      var body: some View {
        Image("apple")
          .resizable()
          .mask(alignment: .center) {
            Circle()
          }
      }
    }

     

    해당 코드처럼 무지개 애플 로고의 이미지를 마스킹 처리했습니다.

    즉, 가운데서부터 마스킹을 하며 원형의 마스크를 생성하여 마스킹 처리를 하도록 코드가 구현되어 있어요.

     

    그럼 어떻게 나타날까요?

     

     

    이렇게 가운데서 해당 이미지 사이즈에 맞게 Circle 사이즈도 동일하며 원형으로 그려지며 원본 애플 로고는 원형에 해당 하는 부분만 보여지고 나머지는 마스킹 처리되어 숨겨지죠.

     

    또 mask는 뷰의 모양뿐만 아니라 불투명도도 제어할 수 있죠.

     

    예시 코드를 볼까요?

     

    import SwiftUI
    
    struct ContentView: View {
      var body: some View {
        Image("apple")
          .mask {
            Rectangle().opacity(0.1)
          }
      }
    }

     

    이렇게 mask로 사각형을 올리고 불투명도를 조절하면 아래와 같이 원본 뷰가 흐릿하게 보여지죠.

     

     

    mask 메서드는 뷰의 Z축 순서에 따라 동작합니다.

    즉, mask 메서드를 호출하는 뷰는 항상 마스크 뷰의 아래에 위치하게 되죠.

    또한, mask 메서드는 뷰의 레이아웃에 영향을 주지 않아요.

    그말인즉슨 mask를 적용하더라도 원본 뷰의 크기와 위치는 불변하다~라는 말입니다 🙋🏻

     

    clipShape도 비슷하게 뷰를 원하는 모양으로 잘라주는데 해당 메서드는 뷰의 모양을 직접적으로 변경시키지만 mask 메서드는 가려줄뿐 직접적으로 원본 뷰의 모양을 바꾸지 않죠!

     

    자, 이렇게 mask를 알아봤는데 제가 원래 하려던거 한번 해볼께요!

     

    바로, 진행율에 따라 해당 이미지를 좌측부터 순차적으로 보여주기!

    말로 설명이 어려울 수 있으니 일단 코드로 보시죠ㅎㅎ

     

    import SwiftUI
    
    struct ContentView: View {
      @State private var percent: CGFloat = 0.0
      
      var body: some View {
        VStack {
          Image("apple")
            .resizable()
            .frame(width: 200, height: 200)
            .mask(ProgressBar(value: $percent))
          
          Slider(value: $percent, in: 0...1)
            .padding()
        }
      }
    }
    
    struct ProgressBar: View {
      @Binding var value: CGFloat
      
      var body: some View {
        GeometryReader { geometry in
          Rectangle()
            .frame(width: geometry.size.width * value, height: geometry.size.height)
        }
      }
    }

     

    이런 코드가 있습니다.

    즉 저는 프로그레스바인 Slider를 직접 조정하여 해당 슬라이더의 값에 따라 애플 로고를 보여주는 정도를 나타내고 싶어요.

    슬라이더의 값이 0이면 애플 로고가 나타지나않고 숨겨져야하며 1에 가까울수록 애플 로고가 완성되는 그림을 원합니다.

    그렇기에 해당 이미지에 mask 메서드를 이용해 ProgressBar라는 하위 뷰를 얹어줍니다.

    해당 뷰는 Rectangle로 되어있고 화면의 전체 넓이, 높이를 가지고 있습니다.

    이제 해당 ProgressBar 뷰에 value가 변하면 그에 따라 비율로 넓이, 높이가 변할거에요!

     

    그리고 이 value의 조정은 Slider를 통해 변경되고 전달됩니다.

     

    한번 돌려볼까요~?

     

     

    이렇게 슬라이더를 조정해서 value 값에 따라 애플 로고가 보여지는 상황이 달라지죠ㅎㅎ

     

    이렇게 슬라이더로 값을 변경해줄 수도 있지만, 보통은 다운로드중 나타내야하면 서버 혹은 타이머를 두어 사용하여 value를 변경할 수도 있겠죠!?

     

    이는 자유롭게 원하는 방식으로 사용하면 될것 같아요.

    중요한건 mask로 뷰를 어떻게 보여주느냐! 이니까요ㅎㅎ

     


    마무리

    trim과 mask를 학습하면서 도형 및 이미지를 자르거나 숨겨서 일부를 보여주는 방법을 알았어요.

    분명히 적절히 사용하면 아주 유용할것 같아요 😁

     


    레퍼런스

     

    trim(from:to:) | Apple Developer Documentation

    Trims this shape by a fractional amount based on its representation as a path.

    developer.apple.com

     

    mask(alignment:_:) | Apple Developer Documentation

    Masks this view using the alpha channel of the given view.

    developer.apple.com

Designed by Tistory.