技术:SwiftUI、SwiftUI4.0、天气、天气App、天气应用程序、雪花效果、降雨效果
运行环境:
SwiftUI4.0 + Xcode14 + MacOS12.6 + iPhone Simulator iPhone 14 Pro Max
SwiftUI创建iOS15 天气应用程序下雪花效果 2/2部分
- 概述
- 详细
- 一、运行效果
- 二、项目结构图
- 三、⚠️程序实现 - 过程 - 基于上一篇文章[【SwiftUI项目】0030、SwiftUI创建iOS15 天气应用程序滚动效果 1/2部分](https://blog.csdn.net/qq_42816425/article/details/126800907) - 扩展一个雪花效果
- 1.创建一个项目命名为 `WeatherAppScrolling`
- 1.1.引入资源文件和颜色
- 2. 创建一个虚拟文件`New Group` 命名为 `View`
- 3. 创建一个虚拟文件`New Group` 命名为 `Model`
- 4. 创建一个文件`New File` 选择`SwiftUI View`类型 命名为`Home`
- 5. 创建一个文件`New File` 选择`SwiftUI View`类型 命名为`CustomStackView`
- 6. 创建一个文件`New File` 选择`SwiftUI View`类型 命名为`CustomCorner` 删除预览视图 并且继承`Shape`
- 7. 创建一个文件`New File` 选择`SwiftUI View`类型 命名为`WeatherDataView`
- 8. 创建一个文件`New File` 选择`SwiftUI View`类型 命名为`Forecast` 删除预览视图 并且继承`Identifiable` 作为模型
- 9. ⭐️创建一个虚拟文件`New Group` 命名为 `SpriteFiles`
- 10. ⭐️创建一个文件`New File` 搜索`Spri` 选择`SpriteKit Particle File`类型 并且`Particle Template`选择为`Rain` 命名为`RainFall`
- 11.⭐️删除`RainFall`的`Assets`资源文件 因为在项目的资源文件里面已经包含了
- 12. ⭐️创建一个文件`New File` 搜索`Spri` 选择`SpriteKit Particle File`类型 并且`Particle Template`选择为`Rain` 命名为`RainFallLanding `
- 13. ⭐️删除`RainFallLanding`的`Assets`资源文件 因为在项目的资源文件里面已经包含了
- Code
概述
基于上一篇文章【SwiftUI项目】0030、SwiftUI创建iOS15 天气应用程序滚动效果 1/2部分 - 扩展一个雪花效果
其中⭐️表示添加的部分
详细
一、运行效果
二、项目结构图
三、⚠️程序实现 - 过程 - 基于上一篇文章【SwiftUI项目】0030、SwiftUI创建iOS15 天气应用程序滚动效果 1/2部分 - 扩展一个雪花效果
思路:
- 搭建主视图 - 滚动视图
- 搭建天气数据
- 自定义栈视图
- 使用自定义栈视图 构建天气数据 - 多个视图
- 使用模型构建未来十天的天气预告数据
- 处理上下滚动
监听偏移量
如果是负数 就处理字体和视图的可见范围- ⭐️新增两个 SpriteKit的下雨文件
- ⭐️滚动偏移计算 雪花落地的效果进行逻辑处理
1.创建一个项目命名为 WeatherAppScrolling
1.1.引入资源文件和颜色
背景1张
⭐️添加雪花效果的资源 - Particle Sprite Atlas
2. 创建一个虚拟文件New Group
命名为 View
3. 创建一个虚拟文件New Group
命名为 Model
4. 创建一个文件New File
选择SwiftUI View
类型 命名为Home
5. 创建一个文件New File
选择SwiftUI View
类型 命名为CustomStackView
主要是: 自定义栈视图 填充每一个指数的内容
6. 创建一个文件New File
选择SwiftUI View
类型 命名为CustomCorner
删除预览视图 并且继承Shape
7. 创建一个文件New File
选择SwiftUI View
类型 命名为WeatherDataView
8. 创建一个文件New File
选择SwiftUI View
类型 命名为Forecast
删除预览视图 并且继承Identifiable
作为模型
9. ⭐️创建一个虚拟文件New Group
命名为 SpriteFiles
10. ⭐️创建一个文件New File
搜索Spri
选择SpriteKit Particle File
类型 并且Particle Template
选择为Rain
命名为RainFall
11.⭐️删除RainFall
的Assets
资源文件 因为在项目的资源文件里面已经包含了
12. ⭐️创建一个文件New File
搜索Spri
选择SpriteKit Particle File
类型 并且Particle Template
选择为Rain
命名为RainFallLanding
13. ⭐️删除RainFallLanding
的Assets
资源文件 因为在项目的资源文件里面已经包含了
Code
ContentView - 主窗口
主要是展示主窗口
Home
//
// ContentView.swift
// Shared
//
// Created by 李宇鸿 on 2022/9/17.
//
import SwiftUI
struct ContentView: View {
var body: some View {
//因为windows在iOS 15中被Decrepted .... .
//获取安全区域使用几何阅读器…
GeometryReader{proxy in
let topEdge = proxy.safeAreaInsets.top
Home(topEdge:topEdge)
.ignoresSafeArea(.all,edges: .top)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
⭐️Home - 主页
思路
- 搭建主视图 - 滚动视图
- 搭建天气数据
- 自定义栈视图
- 使用自定义栈视图 构建天气数据 - 多个视图
- 使用模型构建未来十天的天气预告数据
- 处理上下滚动
监听偏移量
如果是负数 就处理字体和视图的可见范围- ⭐️新增两个 SpriteKit的下雨文件
- ⭐️滚动偏移计算 雪花落地的效果进行逻辑处理
//
// Home.swift
// WeatherAppScrolling (iOS)
//
// Created by 李宇鸿 on 2022/9/18.
//
import SwiftUI
import SpriteKit
struct Home: View {
@State var offset : CGFloat = 0
var topEdge : CGFloat
// 为了避免过早开始着陆动画…
@State var showRain = false
// 我们打算推迟启动它…
var body: some View {
ZStack{
// 获取高度和宽度的Gemetry Reader…
GeometryReader{proxy in
Image("sky")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: proxy.size.width,height: proxy.size.height)
}
.ignoresSafeArea()
// 模糊的效果
.overlay(.ultraThinMaterial)
// ⭐️ 加载雪花
//雨落视图..
//可能是一个bug…
//当滚动被重新启动…
// 避免出现…… 使用GeometryReader 包裹
GeometryReader{ _ in
SpriteView(scene: RainFall(),options:[.allowsTransparency])
}
// 控制是否显示降雪
.opacity(showRain ? 1 : 0)
// 主视图
ScrollView(.vertical,showsIndicators: false){
VStack{
// 天气数据……
VStack(alignment: .center,spacing: 5) {
Text("Bei Jing")
.font(.system(size: 35))
.foregroundStyle(.white)
.shadow(radius: 5)
Text("34°")
.font(.system(size: 45))
.foregroundStyle(.white)
.shadow(radius: 5)
.opacity(getTitleOpactiy())
Text("Cloudy")
.foregroundStyle(.secondary)
.foregroundStyle(.white)
.shadow(radius: 5)
.opacity(getTitleOpactiy())
Text("H:103˚L:105")
.foregroundStyle(.primary)
.foregroundStyle(.white)
.shadow(radius: 5)
.opacity(getTitleOpactiy())
}
.offset(y:-offset)
// 对于底部拖动效果..
.offset(y: offset > 0 ? (offset / UIScreen.main.bounds.width) * 100 : 0)
.offset(y: getTitleOffset())
// 自定义数据视图…
VStack(spacing:8){
// 自定义栈
// 1. 每小时的预测
CustomStackView {
Label {
Text("Hourly Forecast")
} icon: {
Image(systemName: "clock")
}
} contentView: {
// Content
// 内容
ScrollView(.horizontal,showsIndicators: false){
HStack(spacing:15) {
ForecastView(time:"12 PM",celcius:33,image:"sun.min")
ForecastView(time:"1 PM",celcius:32,image:"sun.haze")
ForecastView(time:"2 PM",celcius:30,image:"sun.min")
ForecastView(time:"3 PM",celcius:34,image:"cloud.sun")
ForecastView(time:"4 PM",celcius:31,image:"sun.haze")
}
}
}
.colorScheme(.dark)
WeatherDataView()
}
// ⭐️ 加载雪花落地效果
.overlay(
GeometryReader{ _ in
SpriteView(scene: RainFallLanding(),options:[.allowsTransparency])
// 将雪花落地效果顶在视图的上方
.offset(y: -10)
}
.opacity(showRain ? 1 : 0)
// ⭐️控制雪花落地的顶部
.offset(y:
-(offset + topEdge) > 72
?
-(offset + (72 + topEdge))
:
0
)
)
}
.padding(.top,25)
.padding(.top,topEdge)
.padding([.horizontal,.bottom])
// 获取偏移量
.overlay(
// GeometryReader : 一个容器视图,根据其自身大小和坐标空间定义其内容。
GeometryReader{ proxy -> Color in
let minY = proxy.frame(in: .global).minY
DispatchQueue.main.async {
self.offset = minY
// ⭐️控制雪花落地的顶部
//为什么包含topege…
//因为我们忽略了顶部egde主视图
print("\(minY + topEdge)")
// 约72……
// 因为我们包含了上边缘
// 因此对于较小的设备值也将相同....
}
return Color.clear
}
)
}
}
// ⭐️ 视图加载的时候 进行一个延迟展示降雪效果
.onAppear{
DispatchQueue.main.asyncAfter(deadline: .now() + 2){
withAnimation {
showRain = true
}
}
}
}
//
func getTitleOpactiy()->CGFloat {
let titleOffset = -getTitleOffset()
let progress = titleOffset / 20
let opactiy = 1 - progress
return opactiy
}
// 获取头部偏移
func getTitleOffset()-> CGFloat{
//设置整个标题的最大高度…
//考虑Max = 120…
if offset < 0 {
let progress = -offset / 120
// //因为顶部填充是25....
let newOffset = (progress <= 1.0 ? progress : 1) * 20
return -newOffset
}
return 0
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
// Home()
ContentView()
}
}
struct ForecastView: View {
var time : String
var celcius : CGFloat
var image : String
var body: some View {
VStack(spacing:15){
Text(time)
.font(.callout.bold())
.foregroundColor(.white)
Image(systemName: image)
// 多色
.symbolVariant(.fill)
.symbolRenderingMode(.palette)
.foregroundStyle(.yellow,.white)
.frame(height:30)
Text("\(Int(celcius))°")
.font(.callout.bold())
.foregroundColor(.white)
}
.padding(.horizontal,10)
}
}
// ⭐️
// 创建雨雪效果,就像iOS 15的天气应用…
//现在我们要定制雨的动画。
class RainFall : SKScene {
override func sceneDidLoad() {
size = UIScreen.main.bounds.size
scaleMode = .resizeFill
// 锚点……
anchorPoint = CGPoint(x: 0.5, y: 1)
// bg Color...
// 背景颜色
backgroundColor = .clear
// 创建节点并添加到场景…
let node = SKEmitterNode(fileNamed: "RainFall.sks")!
addChild(node)
// 全宽……
node.particlePositionRange.dx = UIScreen.main.bounds.width
}
}
// ⭐️
// 降雨降落
class RainFallLanding : SKScene {
override func sceneDidLoad() {
size = UIScreen.main.bounds.size
scaleMode = .resizeFill
// 锚点……
let height = UIScreen.main.bounds.height
// 通过解除位置范围得到百分比…
anchorPoint = CGPoint(x: 0.5, y: (height - 5) / height)
// 背景颜色
backgroundColor = .clear
// 创建节点并添加到场景…
let node = SKEmitterNode(fileNamed: "RainFallLanding.sks")!
addChild(node)
// 删除卡片填充…
node.particlePositionRange.dx = UIScreen.main.bounds.width - 30
}
}
CustomStackView - 栈视图 - 每一个天气指数数据UI
//
// CustomStackView.swift
// WeatherAppScrolling (iOS)
//
// Created by 李宇鸿 on 2022/9/19.
//
import SwiftUI
struct CustomStackView<Title:View,Content:View>: View {
var titleView: Title
var contentView : Content
// Offsets....
@State var topOffset : CGFloat = 0
@State var bottomOffset : CGFloat = 0
init(@ViewBuilder titleView:@escaping ()-> Title,@ViewBuilder contentView: @escaping()->Content){
self.contentView = contentView()
self.titleView = titleView()
}
var body: some View {
VStack(spacing: 0) {
titleView
.font(.callout)
.lineLimit(1)
.frame(height:38)
.frame(maxWidth:.infinity,alignment: .leading)
.padding(.leading)
.background(.ultraThinMaterial,in:CustomCorner(corners: bottomOffset < 38 ? .allCorners : [.topLeft,.topRight], radius: 12))
.zIndex(1)
VStack{
// 分隔物
Divider()
contentView
.padding()
}
.background(.ultraThinMaterial,in:CustomCorner(corners: [.bottomLeft,.bottomRight], radius: 12))
// 移动内容向上……
.offset(y: topOffset >= 120 ? 0 : -(-topOffset + 120))
.zIndex(0)
// 剪切以避免背景覆盖
.clipped()
}
.colorScheme(.dark)
.cornerRadius(12)
.opacity(getOpacity())
// 停止视图@120……
.offset(y: topOffset >= 120 ? 0 : -topOffset + 120)
.background(
GeometryReader{proxy -> Color in
let minY = proxy.frame(in: .global).minY
let maxY = proxy.frame(in: .global).maxY
DispatchQueue.main.async {
self.topOffset = minY
// 减少120…
self.bottomOffset = maxY - 120
// print(maxY)
// print(self.bottomOffset)
// 这样我们的标题高度就会是38…
}
return Color.clear
}
)
.modifier(CornerModifier(bottomOffset: $bottomOffset))
}
// 不透明度
func getOpacity()-> CGFloat{
if bottomOffset < 28 {
let progress = bottomOffset / 28
return progress
}
return 1
}
}
struct CustomStackView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// 避免创建新的修饰符…
struct CornerModifier: ViewModifier {
@Binding var bottomOffset : CGFloat
func body(content: Content) -> some View {
if bottomOffset < 38 {
content
}
else {
content
.cornerRadius(12)
}
}
}
CustomCorner - 自定义指定圆角区域
//
// CustomCorner.swift
// WeatherAppScrolling (iOS)
//
// Created by 李宇鸿 on 2022/9/19.
//
import SwiftUI
// 自定义圆角
struct CustomCorner: Shape {
var corners : UIRectCorner
var radius : CGFloat
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
WeatherDataView - 天气数据
用来搭建多个栈视图的天气数据
(空气质量、紫外线指数、降雨、近10天天气数据)
//
// WeatherDataView.swift
// WeatherAppScrolling (iOS)
//
// Created by 李宇鸿 on 2022/9/20.
//
import SwiftUI
struct WeatherDataView: View {
var body: some View {
// 1.空气质量
VStack{
CustomStackView {
Label {
Text("Air Quality")
} icon: {
Image(systemName: "circle.hexagongrid.fill")
}
} contentView: {
VStack(alignment: .leading, spacing: 10) {
// 空气质量指数 以及健康情况
Text("79 - good")
.font(.title3.bold())
Text("Air quality is acceptable, but some pollutants may have a weak impact on the health of a very small number of unusually sensitive people")
.fontWeight(.semibold)
}
.foregroundStyle(.white)
}
}
// 2. 紫外线指数
HStack{
CustomStackView {
Label {
Text("UV Index")
} icon: {
Image(systemName: "sun.min")
}
} contentView: {
VStack(alignment: .leading, spacing: 10) {
Text("0")
.font(.title)
.fontWeight(.semibold)
Text("Low")
.font(.title)
.fontWeight(.semibold)
}
.foregroundStyle(.white)
.frame(maxWidth: .infinity,maxHeight: .infinity, alignment: .leading)
}
// 3. 降雨
CustomStackView {
Label {
Text("Rainfall")
} icon: {
Image(systemName: "drop.fill")
}
} contentView: {
VStack(alignment: .leading, spacing: 10) {
Text("0 mm")
.font(.title)
.fontWeight(.semibold)
Text("in last 24 hours")
.font(.title3)
.fontWeight(.semibold)
}
.foregroundStyle(.white)
.frame(maxWidth: .infinity,maxHeight: .infinity, alignment: .leading)
}
}
.frame(maxHeight: .infinity)
// 4.最近10天天气
CustomStackView {
Label {
Text("10-Day Forecast")
} icon: {
Image(systemName: "calendar")
}
} contentView: {
VStack(alignment: .leading, spacing: 10) {
ForEach(forecast){cast in
VStack {
HStack(spacing: 15){
Text(cast.day)
.font(.title3.bold())
.foregroundStyle(.white)
// 最大宽度
.frame(width: 60,alignment: .leading)
Image(systemName: cast.image)
.font(.title3)
.symbolVariant(.fill)
.symbolRenderingMode(.palette)
.foregroundStyle(.yellow,.white)
.frame(width: 30)
Text("\(Int(cast.farenheit - 8))")
.font(.title3.bold())
.foregroundStyle(.secondary)
.foregroundStyle(.white)
// 进度条……
ZStack(alignment: .leading) {
Capsule()
.fill(.tertiary)
.foregroundStyle(.white)
// 宽度…
GeometryReader{proxy in
Capsule()
.fill(.linearGradient(.init(colors: [.orange,.red]), startPoint: .leading, endPoint: .trailing))
.frame(width: (cast.farenheit / 45) * proxy.size.width)
}
}
.frame(height: 4)
Text("\(Int(cast.farenheit))˚")
.font(.title3.bold())
.foregroundStyle(.white)
}
Divider()
}
.padding(.vertical,8)
}
}
}
}
}
struct WeatherDataView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Forecast - 模型
//
// Forecast.swift
// WeatherAppScrolling (iOS)
//
// Created by 李宇鸿 on 2022/9/20.
//
import SwiftUI
// 样品模型和十天数据....
struct DayForecast: Identifiable{
var id = UUID().uuidString
var day: String
var farenheit: CGFloat
var image: String
}
var forecast = [
DayForecast(day: "Today", farenheit: 34,image: "sun.min"),
DayForecast(day: "Wed", farenheit: 33,image: "cloud.sun"),
DayForecast(day: "Tue", farenheit: 32,image: "cloud.sun.bolt"),
DayForecast(day: "Thu", farenheit: 30,image: "sun.max"),
DayForecast(day: "Fri", farenheit: 31,image: "cloud.sun"),
DayForecast(day: "Sat", farenheit: 30,image: "cloud.sun"),
DayForecast(day: "Sun", farenheit: 33,image: "sun.max"),
DayForecast(day: "Mon", farenheit: 34,image: "sun.max"),
DayForecast(day: "Tue", farenheit: 31,image: "cloud.sun.bolt"),
DayForecast(day: "Wed", farenheit: 29,image: "sun.min"),
]
⭐️RainFall - 自定义雪花效果
- 设置背景颜色透明度
- 修改Rain参数表
- 设置雪花的颜色