SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二)

版本记录

版本号 时间
V1.0 2019.12.11 星期三

前言

今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)
5. SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)
6. SwiftUI框架详细解析 (六) —— 基于SwiftUI的导航的实现(一)
7. SwiftUI框架详细解析 (七) —— 基于SwiftUI的导航的实现(二)
8. SwiftUI框架详细解析 (八) —— 基于SwiftUI的动画的实现(一)

源码

1. Swift

首先看下工程组织结构

SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二)_第1张图片

接下来就是源码了

1. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  // MARK: - UISceneSession Lifecycle
  
  func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    return UISceneConfiguration(name: "Default Configuration",
                                sessionRole: connectingSceneSession.role)
  }
}
2. SceneDelegate.swift
import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  
  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    if let windowScene = scene as? UIWindowScene {
      let window = UIWindow(windowScene: windowScene)
      window.rootViewController = UIHostingController(rootView: ContentView())
      self.window = window
      window.makeKeyAndVisible()
    }
  }
}
3. ContentView.swift
import SwiftUI

extension AnyTransition {
  static var customTransition: AnyTransition {
    let insertion = AnyTransition.move(edge: .top)
      .combined(with: .scale(scale: 0.2, anchor: .topTrailing))
      .combined(with: .opacity)
    let removal = AnyTransition.move(edge: .top)
    return .asymmetric(insertion: insertion, removal: removal)
  }
}

struct ContentView: View {
  @State var showMoon: String? = nil
  let moonAnimation = Animation.default

  func toggleMoons(_ name: String) -> Bool {
    return name == showMoon
  }

  var body: some View {
    List(planets) { planet in
      self.makePlanetRow(planet: planet)
    }
  }

  func makePlanetRow(planet: Planet) -> some View {
    VStack {
      HStack {
        Image(planet.name)
          .resizable()
          .aspectRatio(1, contentMode: .fit)
          .frame(height: 60)
        Text(planet.name)
        Spacer()
        if planet.hasMoons {
          Button(action: {
            withAnimation(.easeInOut) {
              self.showMoon = self.toggleMoons(planet.name) ? nil : planet.name
            }
          }) {
            Image(systemName: "moon.circle.fill")
              .rotationEffect(.degrees(self.toggleMoons(planet.name) ? -50 : 0))
              .animation(nil)
              .scaleEffect(self.toggleMoons(planet.name) ? 2 : 1)
              .animation(moonAnimation)
          }
        }
      }
      if self.toggleMoons(planet.name) {
        MoonList(planet: planet)
          .transition(.customTransition)
      }
    }
  }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
#endif
4. MoonList.swift
import SwiftUI

struct MoonList: View {
  let planet: Planet
  @State private var showModal = false
  @State private var selectedMoon: Moon?

  var body: some View {
    VStack {
      SolarSystem(planet: planet)
        .frame(height: 160)
      ScrollView(.horizontal, showsIndicators: false) {
        HStack(spacing: 20) {
          ForEach(planet.moons) { moon in
            Button(action: {
              self.showModal = true
              self.selectedMoon = moon
            }) {
              HStack {
                Image(systemName: "moon")
                Text(moon.name)
              }.sheet(isPresented: self.$showModal) {
                PlanetInfo(planet: self.planet, startingMoon: self.selectedMoon!)
              }
            }
          }
        }
      }
    }
  }
}

#if DEBUG
struct MoonList_Previews: PreviewProvider {
  static var previews: some View {
    MoonList(planet: planets[5])
      .frame(width: 320)
  }
}
#endif
5. SolarSystem.swift
import SwiftUI

struct SolarSystem: View {
  var moons: [Moon] { planet.moons }
  let planet: Planet
  @State private var animationFlag = false

  var body: some View {
    GeometryReader { geometry in
      self.makeSystem(geometry)
    }
  }

  func moonPath(planetSize: CGFloat, radiusIncrement: CGFloat, index: CGFloat) -> some View {
    return Circle()
      .stroke(Color.gray)
      .frame(width: planetSize + radiusIncrement * index,
             height: planetSize + radiusIncrement * index)
  }

  func moon(planetSize: CGFloat,
            moonSize: CGFloat,
            radiusIncrement: CGFloat,
            index: CGFloat) -> some View {
    return Circle()
      .fill(Color.orange)
      .frame(width: moonSize, height: moonSize)
  }

  func makeSystem(_ geometry: GeometryProxy) -> some View {
    let planetSize = geometry.size.height * 0.25
    let moonSize = geometry.size.height * 0.1
    let radiusIncrement = (geometry.size.height - planetSize - moonSize) / CGFloat(moons.count)
    let range = 1 ... moons.count
    return
      ZStack {
        Circle()
          .fill(planet.drawColor)
          .frame(width: planetSize, height: planetSize, alignment: .center)

        ForEach(range, id: \.self) { index in
          // orbit paths
          self.moonPath(planetSize: planetSize, radiusIncrement: radiusIncrement, index: CGFloat(index))
        }
        ForEach(range, id: \.self) { index in
          // individual "moon" circles
          self.moon(planetSize: planetSize, moonSize: moonSize, radiusIncrement: radiusIncrement, index: CGFloat(index))
              .modifier(self.makeOrbitEffect(
                diameter: planetSize + radiusIncrement * CGFloat(index)
              ))
              .animation(Animation
                .linear(duration: Double.random(in: 10 ... 100))
                .repeatForever(autoreverses: false)
              )
        }
    }
    .onAppear {
      self.animationFlag.toggle()
    }
  }

  func animation(index: Double) -> Animation {
    return Animation.spring(response: 0.55, dampingFraction: 0.45, blendDuration: 0)
      .speed(2)
      .delay(0.075 * index)
  }

  func makeOrbitEffect(diameter: CGFloat) -> some GeometryEffect {
    return OrbitEffect(angle: self.animationFlag ? 2 * .pi : 0,
                       radius: diameter / 2.0)
  }
}

struct OrbitEffect: GeometryEffect {
  let initialAngle = CGFloat.random(in: 0 ..< 2 * .pi)

  var angle: CGFloat = 0
  let radius: CGFloat

  var animatableData: CGFloat {
    get { return angle }
    set { angle = newValue }
  }

  func effectValue(size: CGSize) -> ProjectionTransform {
    let pt = CGPoint(x: cos(angle + initialAngle) * radius,
                     y: sin(angle + initialAngle) * radius)
    let translation = CGAffineTransform(translationX: pt.x, y: pt.y)
    return ProjectionTransform(translation)
  }
}

#if DEBUG
struct SolarSystem_Previews: PreviewProvider {
  static var previews: some View {
    SolarSystem(planet: planets[5])
      .frame(width: 320, height: 240)
  }
}
#endif
6. MoonView.swift
import SwiftUI

struct MoonView: View {
  @State var angle: CGFloat = -CGFloat.pi / 2
  let size: CGFloat
  let radius: CGFloat
  let targetAngle: CGFloat

  init(angle: CGFloat, size: CGFloat, radius: CGFloat) {
    self.targetAngle = angle
    self.size = size
    self.radius = radius
    self.angle = angle
  }

  var body: some View {
    return Circle()
      .fill(Color.orange)
      .frame(width: size, height: size)
      .offset(x: radius * cos(angle),
              y: radius * sin(angle))
      .onAppear {
        withAnimation {
          self.angle = self.targetAngle
        }
      }
  }
}
7. PlanetInfo.swift
import SwiftUI

struct PlanetInfo: View {
  @Environment(\.presentationMode) var presentation
  let planet: Planet
  let startingMoon: Moon

  var numberFormatter: NumberFormatter = {
    let nf = NumberFormatter()
    nf.numberStyle = .decimal
    nf.usesGroupingSeparator = true
    nf.maximumFractionDigits = 0
    return nf
  }()

  var bigNumberFormatter: NumberFormatter = {
    let nf = NumberFormatter()
    nf.numberStyle = .scientific
    nf.usesGroupingSeparator = true
    nf.maximumFractionDigits = 0
    return nf
  }()

  var body: some View {
    VStack(alignment: .leading) {
      HStack {
        Text(planet.name)
          .font(.largeTitle)
        Spacer()
        Button("Done") {
          self.presentation.wrappedValue.dismiss()
        }
      }.padding()
      MoonFlow(selectedMoon: startingMoon, moons: planet.moons)
        .frame(height:200)
      Text("Radius: \(numberFormatter.string(for: planet.radius)!)km").padding()
      Text("Weight: \(bigNumberFormatter.string(for: planet.weight)!)kg").padding()
      Text("Gravity: \(planet.gravity)g").padding()
      Spacer()
    }
  }
}
8. MoonFlow.swift
import SwiftUI

struct MoonFlow: View {
  @State var selectedMoon: Moon
  var moons: [Moon]

  var body: some View {
    GeometryReader { geometry in
      ScrollView(.horizontal) {
        HStack(spacing: 20) {
          ForEach(self.moons) { moon in
            GeometryReader { moonGeometry in
              VStack{
                Image(uiImage: moon.image)
                  .resizable()
                  .frame(width:120, height: 120)
                Text(moon.name)
              }
              .rotation3DEffect(
                .degrees(Double(moonGeometry.frame(in: .global).midX - geometry.size.width / 2) / 3),
                axis: (x: 0, y: 1, z: 0)
              )
            }.frame(width:120)
          }
        }
        .frame(minWidth: geometry.size.width)
      }
      .frame(width: geometry.size.width)
    }
  }
}

#if DEBUG
struct MoonFlow_Previews: PreviewProvider {
  static var previews: some View {
    MoonFlow(selectedMoon: planets[5].moons[0], moons: planets[5].moons)
  }
}
#endif
9. PlanetModel.swift
import SwiftUI

let planets = [
  Planet(name: "Mercury",
         moons: [],
         radius: 2_439.7,
         weight: 3.3011e23,
         gravity: 0.38,
         drawColor: .gray),
  Planet(name: "Venus",
         moons: [],
         radius: 6_051.8,
         weight: 4.8675e24,
         gravity: 0.904,
         drawColor: .yellow),
  Planet(name: "Earth",
         moons: [Moon(name: "Luna")],
         radius: 6_371,
         weight: 5.97237e24,
         gravity: 1,
         drawColor: .blue),
  Planet(name: "Mars",
         moons: [Moon(name: "Phobos"),
                 Moon(name: "Deimos")],
         radius: 3_389.5,
         weight: 6.4171e23,
         gravity: 0.3794,
         drawColor: .red),
  Planet(name: "Jupiter",
         moons: [Moon(name: "Ganymede"),
                 Moon(name: "Callisto"),
                 Moon(name: "Europa"),
                 Moon(name: "Amalthea"),
                 Moon(name: "Himalia"),
                 Moon(name: "Thebe"),
                 Moon(name: "Elara")],
         radius: 69_911,
         weight: 1.8982e27,
         gravity: 2.528,
         drawColor: .orange),
  Planet(name: "Saturn",
         moons: [Moon(name: "Titan"),
                 Moon(name: "Rhea"),
                 Moon(name: "Iapetus"),
                 Moon(name: "Dione"),
                 Moon(name: "Tethys"),
                 Moon(name: "Enceladus"),
                 Moon(name: "Mimas"),
                 Moon(name: "Hyperion"),
                 Moon(name: "Phoebe"),
                 Moon(name: "Janus")],
         radius: 60_268,
         weight: 5.6834e26,
         gravity: 1.065,
         drawColor: .yellow),
  Planet(name: "Uranus",
         moons: [Moon(name: "Titania"),
                 Moon(name: "Oberon"),
                 Moon(name: "Umbriel"),
                 Moon(name: "Ariel"),
                 Moon(name: "Miranda")],
         radius: 25_362,
         weight: 8.6810e25,
         gravity: 0.886,
         drawColor: .blue),
  Planet(name: "Neptune",
         moons: [Moon(name: "Triton"),
                 Moon(name: "Proteus"),
                 Moon(name: "Nereid"),
                 Moon(name: "Larissa"),
                 Moon(name: "Galatea")],
         radius: 24_622,
         weight: 1.02413e26,
         gravity: 1.14,
         drawColor: .blue)
]

struct Planet {
  let name: String
  let moons: [Moon]
  let radius: Double
  let weight: Double
  let gravity: Double
  let drawColor: Color

  var hasMoons: Bool { !moons.isEmpty }
}

extension Planet: Identifiable {
  var id: String {
    return name
  }
}

struct Moon {
  let name: String

  var image: UIImage {
    let path = Bundle.main.path(forResource: "\(name)".lowercased(), ofType: "jpg")
    if let path = path, let image = UIImage(contentsOfFile: path) {
      return image
    } else {
      return UIImage(contentsOfFile: Bundle.main.path(forResource: "titan".lowercased(), ofType: "jpg")!)!
    }
  }
}

extension Moon: Identifiable {
  var id: String {
    return name
  }
}

extension Moon: Equatable {}

后记

本篇主要讲述了基于SwiftUI的动画的实现,感兴趣的给个赞或者关注~~~

SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二)_第2张图片

你可能感兴趣的:(SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二))