アニメーションをつける
チュートリアルより アニメーションは.animationで簡単につけられる。 .animation(nil)でアニメーションを切ることができる。 withAnimationを使うと、ブロック内にアニメーションがつけられる。 遷移時の遷移方法ははtransisionで指定できる。 transisionはAnyTransitionとして自作できる。 例えば表示されるときと消えるときで次のように定義できる。
extension AnyTransition { static var moveAndFade: AnyTransition { let insertion = AnyTransition.move(edge: .trailing) let removal = AnyTransition.scale .combined(with: .opacity) return asymmetric(insertion: insertion, removal: removal) } }
チュートリアルではボタンを押すとグラフがあらわれ、グラフが波打つ。
まず、グラフにつかうデータモデル
struct Hike: Codable, Hashable, Identifiable { var name: String var id: Int var distance: Double var difficulty: Int var observations: [Observation] static var formatter = LengthFormatter() var distanceText: String { return 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> } }
そしてグラフの描画部分
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 { return range.upperBound - range.lowerBound } // 波打つアニメーションrippleを定義、これはスプリングを減衰率を指定し、ゆっくり、また値に応じたdelayをつけるという工夫をしている。 extension Animation { static func ripple(index:Int) -> Animation { Animation.spring( dampingFraction: 0.5) .speed(2) .delay(0.03 * Double(index)) } } struct HikeGraph: View { var hike: Hike var path: KeyPath<Hike.Observation, Range<Double>> 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 } } var body: some View { let data = hike.observations let overallRange = rangeOfRanges(data.lazy.map { $0[keyPath: self.path] }) let maxMagnitude = data.map { magnitude(of: $0[keyPath: path]) }.max()! let heightRatio = (1 - CGFloat(maxMagnitude / magnitude(of: overallRange))) / 2 return GeometryReader { proxy in HStack(alignment: .bottom, spacing: proxy.size.width / 120) { ForEach(data.indices) { index in GraphCapsule( index: index, height: proxy.size.height, range: data[index][keyPath: self.path], overallRange: overallRange) .colorMultiply(self.color) .transition(.slide) // スライド .animation(.ripple(index: index)) // ここで波打つ } .offset(x: 0, y: proxy.size.height * heightRatio) } } } }
GraphCapsuleというのはひとまず、無視するが一応
struct GraphCapsule: View { var index: Int var height: CGFloat var range: Range<Double> var overallRange: Range<Double> var heightRatio: CGFloat { max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15) } var offsetRatio: CGFloat { CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange)) } var body: some View { Capsule() .fill(Color.white) .frame(height: height * heightRatio) .offset(x: 0, y: height * -offsetRatio) } } struct GraphCapsule_Previews: PreviewProvider { static var previews: some View { GraphCapsule(index: 0, height: 150, range: 10..<50, overallRange: 0..<100) } }
グラフを表示する詳細画面 dataToShowを動的に変える。
struct HikeDetail: View { let hike: Hike @State var dataToShow = \Hike.Observation.elevation var buttons = [ ("Elevation", \Hike.Observation.elevation), ("Heart Rate", \Hike.Observation.heartRate), ("Pace", \Hike.Observation.pace), ] var body: some View { return VStack { HikeGraph(hike: hike, path: dataToShow) .frame(height: 200) HStack(spacing: 25) { ForEach(buttons, id: \.0) { value in Button(action: { self.dataToShow = value.1 }) { Text(value.0) .font(.system(size: 15)) .foregroundColor(value.1 == self.dataToShow ? Color.gray : Color.accentColor) .animation(nil) } } } } } }
項目からタップしてグラフを出すようにする。 transisionを定義して使っている
extension AnyTransition { static var moveAndFade: AnyTransition { let insertion = AnyTransition.move(edge: .trailing) let removal = AnyTransition.scale .combined(with: .opacity) return asymmetric(insertion: insertion, removal: removal) } } struct HikeView: View { var hike: Hike @State private var showDetail = true var body: some View { VStack { HStack { HikeGraph(hike: hike, path: \.elevation) .frame(width: 50, height: 30) .animation(nil) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { withAnimation { self.showDetail.toggle() } }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5:1.0) .padding() } } if showDetail { HikeDetail(hike: hike) .transition(.moveAndFade) } } } }