관리 메뉴

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

[iOS - SwiftUI] 튜토리얼 - 8. GeometryReader를 이용한 뷰 구현 본문

iOS 튜토리얼 (SwiftUI)

[iOS - SwiftUI] 튜토리얼 - 8. GeometryReader를 이용한 뷰 구현

jake-kim 2022. 7. 10. 22:24

GeometryReader

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

  • ContainerView이며, 내부에 UIView들의 layout을 쉽게 변경할 수 있는 역할

* GeometryReader를 안쓴 경우) Stack안에 두가지의 뷰가 들어가고, Rectangle이 하단의 모든 자리를 차지하는 형태

struct Example: View {
  var body: some View {
    VStack {
      Text("example GeoMetryReader")
      Rectangle()
        .foregroundColor(.green)
    }
  }
}

struct Example_Previews: PreviewProvider {
  static var previews: some View {
    Example()
  }
}

GeometryReader를 사용한 경우)

  • GeometryReader의 closure를 통해서, containerView의 size를 접근할 수 있어 사용이 가능

struct Example: View {
  var body: some View {
    VStack {
      Text("example GeoMetryReader")
      GeometryReader { proxy in // <-
        Rectangle()
          .foregroundColor(.green)
          .frame(width: proxy.size.width / 2, height: proxy.size.height / 2, alignment: .center)

      }
    }
  }
}

GeometryReader는 여전히 영역을 차지하고 있는중 (= ContainerView역할)

튜토리얼 - GeometryReader를 이용한 구현

* 코드 - https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions

  • 위 그림에 사용될 데이터 모델 준비 `Hike`
struct Hike: Codable, Hashable, Identifiable {
  var id: Int
  var name: String
  var distance: Double
  var difficulty: Int
  var observations: [Observation]
  
  static var formatter = LengthFormatter()
  
  var distanceText: String {
    Hike.formatter
      .string(fromValue: distance, unit: .kilometer)
  }
  
  struct Observation: Codable, Hashable {
    var distanceFromStart: Double
    
    var elevation: Range<Double>
    var pace: Range<Double>
    var heartRate: Range<Double>
  }
}
  • HikeGraph 뷰 구현
    • Hike 데이터와 path 정보를 받는 뷰
    • path에 KeyPath를 사용하는 이유?
      • KeyPath를 사용하면, 마치 property를 enum타입의 case와 같이 사용이 가능 (switch로 분류하여 각각 property에 해당되는 값 분기가 가능)
struct HikeGraph: View {
  var hike: Hike
  var path: KeyPath<Hike.Observation, Range<Double>>

  // TODO
}

* KeyPath 사용법

  • 첫번째 인수 값(Hike.Observation) 사용 - 조건문에서 사용 `(path == \.elevation)`
  • 두번째 인수 값 사용 - `인스턴스[keyPath: path]` (각 프로퍼티에 해당되는 값을 얻을 수 있음)
var path: KeyPath<Hike.Observation, Range<Double>>


// 초기화 하는 쪽 - keyPath로 주입
HikeGraph(hike: hike, path: \Hike.Observation.elevation)
  • KeyPath를 통해 분기하여 각 property마다 색상을 다르게 적용
  // in HikeGraph
  var color: Color {
    switch path {
    case \.elevation:
      return .gray
    case \.heartRate:
      return Color(hue: 0, saturation: 0.5, brightness: 0.7)
    case \.pace:
      return Color(hue: 0.7, saturation: 0.4, brightness: 0.7)
    default:
      return .black
    }
  }
  • 각 데이터를 처리하여 적절한 값을 가져오게하는 헬퍼함수 정의 (별로 안중요한 부분)
func rangeOfRanges<C: Collection>(_ ranges: C) -> Range<Double>
where C.Element == Range<Double> {
  guard !ranges.isEmpty else { return 0..<0 }
  let low = ranges.lazy.map { $0.lowerBound }.min()!
  let high = ranges.lazy.map { $0.upperBound }.max()!
  return low..<high
}

func magnitude(of range: Range<Double>) -> Double {
  range.upperBound - range.lowerBound
}
  • 뷰 구현 (HikeGraph의 body)
    • GeometryReader 사용

GeometryReader 영역
GeometryReader 안에 있는 뷰

* GeometryReader는 이전 포스팅 글에서 구현한 GraphCapsule 뷰

  • body 부분 구현
  // in HikeGraph
  var body: some View {
    let data = hike.observations
    let overallRange = rangeOfRanges(data.lazy.map { $0[keyPath: path] })
    let maxMagnitude = data.map { magnitude(of: $0[keyPath: path]) }.max()!
    let heightRatio = 1 - CGFloat(maxMagnitude / magnitude(of: overallRange))
    
    return GeometryReader { ... }
  • GeometryReader 구현
    • GeometryReader 부분에서 proxy를 사용하여, HStack의 spacing 크기와 내부 height계산에 사용
    // GeometryReader: 그 자체로 View이고 container 안 View 스스로의 크기와 위치를 함수로 정의
    return GeometryReader { proxy in
      HStack(alignment: .bottom, spacing: proxy.size.width / 120) {
        ForEach(Array(data.enumerated()), id: \.offset) { index, observation in
          GraphCapsule(
            index: index,
            color: color,
            height: proxy.size.height,
            range: observation[keyPath: path],
            overallRange: overallRange
          )
            .animation(.ripple(index: index))
        }
        .offset(x: 0, y: proxy.size.height * heightRatio)
      }
    }
  }
  • Previews 정의
struct HikeGraph_Previews: PreviewProvider {
  static var hike = ModelData().hikes[0]
  
  static var previews: some View {
    Group {
      HikeGraph(hike: hike, path: \.elevation)
        .frame(height: 200)
      HikeGraph(hike: hike, path: \.heartRate)
        .frame(height: 200)
      HikeGraph(hike: hike, path: \.pace)
        .frame(height: 200)
    }
  }
}

preview1
preview2
preview3

* 참고

https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions

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

 

Comments