Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save CodeSlicing/6339c7bcc36a1fecac242e5a6de59bae to your computer and use it in GitHub Desktop.
Save CodeSlicing/6339c7bcc36a1fecac242e5a6de59bae to your computer and use it in GitHub Desktop.
Native Source code for CodeSlicing episode on Animated Weather Icons Part 5 - The Big Reveal
//
// WeatherIconCloudSunRainFinalDemoNative.swift
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
// AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// Created by Adam Fordyce on 15/05/2021.
// Copyright © 2021 Adam Fordyce. All rights reserved.
//
import SwiftUI
private struct WeatherIconCloudSunRainFinalDemoNative: View {
@State private var showingCloud = false
@State private var showingSun = false
@State private var showingRain = false
@State private var showingRays = false
var body: some View {
GeometryReader { (geo: GeometryProxy) in
ZStack {
SunView(showingRays: showingRays)
.scaleEffect(0.7)
.scaleEffect(showingSun ? 1 : 0)
.opacity(showingSun ? 1 : 0)
.offset(x: geo.size.width * 0.15, y: geo.size.height * -0.15)
RainView()
.scaleEffect(0.7)
.opacity(showingRain ? 1 : 0)
.rotationEffect(.degrees(30))
.offset(x: geo.size.width * -0.2, y: geo.size.height * 0.26)
CloudView()
.scaleEffect(0.7)
.scaleEffect(showingCloud ? 1 : 0)
.opacity(showingCloud ? 1 : 0)
.offset(x: geo.size.width * -0.15, y: geo.size.height * -0.03)
}
.onAppear {
withAnimation(.easeInOut) {
showingCloud = true
}
Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false) { timer in
withAnimation(.easeInOut) {
showingSun = true
}
withAnimation(.easeInOut(duration: 1)) {
showingRain = true
}
}
Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { timer in
withAnimation(.spring(response: 0.6, dampingFraction: 0.6)) {
showingRays = true
}
}
}
}
}
}
private struct CloudElement: View {
var body: some View {
GeometryReader { (geo: GeometryProxy) in
CloudView()
.scaleEffect(0.7)
.offset(x: geo.size.width * -0.15, y: geo.size.height * -0.03)
}
}
}
private struct SunElement: View {
let showingRays: Bool
var body: some View {
GeometryReader { (geo: GeometryProxy) in
SunView(showingRays: showingRays)
.scaleEffect(0.7)
.offset(x: geo.size.width * 0.15, y: geo.size.height * -0.15)
}
}
}
private struct RainElement: View {
var body: some View {
GeometryReader { (geo: GeometryProxy) in
RainView()
.scaleEffect(0.7)
.rotationEffect(.degrees(30))
.offset(x: geo.size.width * -0.19, y: geo.size.height * 0.23)
}
}
}
private struct CloudView: View {
var body: some View {
let debug = false
CloudShape(debug: debug)
.styling()
}
}
private struct CloudShape: Shape {
var debug = false
func path(in rect: CGRect) -> Path {
var path = Path()
let startingPoint = CGPoint(x: rect.minX + rect.width * 0.17, y: rect.minY + rect.height * 0.833)
path.move(to: startingPoint)
let c1destination = CGPoint(x: rect.minX + rect.width * 0.141, y: rect.minY + rect.height * 0.465)
let c1cp1 = CGPoint(x: rect.minX + rect.width * -0.05, y: rect.minY + rect.height * 0.81)
let c1cp2 = CGPoint(x: rect.minX + rect.width * -0.05, y: rect.minY + rect.height * 0.5)
let c2destination = CGPoint(x: rect.minX + rect.width * 0.737, y: rect.minY + rect.height * 0.35)
let c2cp1 = CGPoint(x: rect.minX + rect.width * 0.17, y: rect.minY + rect.height * 0.13)
let c2cp2 = CGPoint(x: rect.minX + rect.width * 0.6, y: rect.minY + rect.height * 0.06)
let c3destination = CGPoint(x: rect.minX + rect.width * 0.773, y: rect.minY + rect.height * 0.833)
let c3cp1 = CGPoint(x: rect.minX + rect.width * 1.08, y: rect.minY + rect.height * 0.35)
let c3cp2 = CGPoint(x: rect.minX + rect.width * 1.08, y: rect.minY + rect.height * 0.8)
path.addCurve(to: c1destination, control1: c1cp1, control2: c1cp2)
path.addCurve(to: c2destination, control1: c2cp1, control2: c2cp2)
path.addCurve(to: c3destination, control1: c3cp1, control2: c3cp2)
path.closeSubpath()
return path
}
@ViewBuilder func styling() -> some View {
if debug {
self.stroke(Color.black, style: .init(lineWidth: 2, lineCap: .round, lineJoin: .round))
} else {
self.fill(
LinearGradient(
gradient: Gradient(colors: [Color(white: 0.7), Color(white: 0.3)]),
startPoint: .top,
endPoint: .bottom))
}
}
}
private struct SunView: View {
let showingRays: Bool
@State private var rotatingRays = false
var body: some View {
GeometryReader { (geo: GeometryProxy) in
let rayWidth = geo.size.width * 0.08
let rayHeight = geo.size.height * 0.2
let rayOffset = geo.size.width * -0.4
let sunDiameter = geo.size.width * 0.45
ZStack {
ForEach(0..<8) { rayIndex in
Capsule()
.fill(Color.yellow)
.frame(width: rayWidth, height: rayHeight)
.offset(y: showingRays ? rayOffset : 0)
.rotationEffect(.degrees(Double(45 * rayIndex)))
}
.rotationEffect(.degrees(rotatingRays ? 360 : 0))
Circle()
.fill(Color.yellow)
.frame(width: sunDiameter, height: sunDiameter)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onAppear {
withAnimation(Animation.linear(duration: 8).repeatForever(autoreverses: false)) {
rotatingRays = true
}
}
}
}
private struct RainView: View {
@State private var animating = false
var body: some View {
GeometryReader { (geo: GeometryProxy) in
RainShape(animatableData: animating ? 1 : 0)
.stroke(Color.blue, style: .init(lineWidth: geo.size.width * 0.07, lineCap: .round))
}
.onAppear {
withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
animating = true
}
}
}
}
private struct RainShape: Shape {
var animatableData: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
let yOffset = rect.height * 0.35
let animationOffset = yOffset * animatableData
let col1X = rect.minX + rect.width * 0.3
let col2X = rect.minX + rect.width * 0.5
let col3X = rect.minX + rect.width * 0.7
for row in 0...2 {
let rowYOffset = CGFloat(row) * yOffset + animationOffset
let drop1Start = CGPoint(x: col1X, y: rect.minY + rect.height * 0.15 + rowYOffset)
let drop1End = CGPoint(x: col1X, y: rect.minY + rect.height * 0.28 + rowYOffset)
path.move(to: drop1Start)
path.addLine(to: drop1End)
let drop2Start = CGPoint(x: col2X, y: rect.minY + rect.height * 0 + rowYOffset)
let drop2End = CGPoint(x: col2X, y: rect.minY + rect.height * 0.13 + rowYOffset)
path.move(to: drop2Start)
path.addLine(to: drop2End)
let drop3Start = CGPoint(x: col3X, y: rect.minY + rect.height * 0.08 + rowYOffset)
let drop3End = CGPoint(x: col3X, y: rect.minY + rect.height * 0.2 + rowYOffset)
path.move(to: drop3Start)
path.addLine(to: drop3End)
}
return path
}
}
struct WeatherIconCloudSunRainFinalDemoNative_Previews: PreviewProvider {
struct WeatherIconCloudSunRainFinalDemoNative_Harness: View {
var body: some View {
WeatherIconCloudSunRainFinalDemoNative()
.frame(width: 300, height: 300)
.padding()
.background(RoundedRectangle(cornerRadius: 20).fill(Color.white))
.clipShape(RoundedRectangle(cornerRadius: 20))
.shadow(radius: 5)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
}
}
static var previews: some View {
WeatherIconCloudSunRainFinalDemoNative_Harness()
.previewDevice("iPhone 12 Pro Max")
.previewDisplayName("iPhone 12 Pro Max")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment