SwiftUI框架詳細(xì)解析 (九) —— 基于SwiftUI的動(dòng)畫(huà)的實(shí)現(xiàn)(二)

版本記錄

版本號(hào) 時(shí)間
V1.0 2019.12.11 星期三

前言

今天翻閱蘋(píng)果的API文檔,發(fā)現(xiàn)多了一個(gè)框架SwiftUI,這里我們就一起來(lái)看一下這個(gè)框架。感興趣的看下面幾篇文章。
1. SwiftUI框架詳細(xì)解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細(xì)解析 (二) —— 基于SwiftUI的閃屏頁(yè)的創(chuàng)建(一)
3. SwiftUI框架詳細(xì)解析 (三) —— 基于SwiftUI的閃屏頁(yè)的創(chuàng)建(二)
4. SwiftUI框架詳細(xì)解析 (四) —— 使用SwiftUI進(jìn)行蘋(píng)果登錄(一)
5. SwiftUI框架詳細(xì)解析 (五) —— 使用SwiftUI進(jìn)行蘋(píng)果登錄(二)
6. SwiftUI框架詳細(xì)解析 (六) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(一)
7. SwiftUI框架詳細(xì)解析 (七) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(二)
8. SwiftUI框架詳細(xì)解析 (八) —— 基于SwiftUI的動(dòng)畫(huà)的實(shí)現(xiàn)(一)

源碼

1. Swift

首先看下工程組織結(jié)構(gòu)

接下來(lái)就是源碼了

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的動(dòng)畫(huà)的實(shí)現(xiàn),感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容