【SwiftUI项目】0030、SwiftUI创建iOS15 天气应用程序下雪花效果 2/2部分

本文详细介绍了如何使用SwiftUI构建一个iOS天气应用程序,特别是添加了雪花和降雨效果。通过创建多个SwiftUI视图组件,如Home、CustomStackView、WeatherDataView等,并结合SpriteKit粒子效果,实现了天气滚动、字体透明度变化以及雪花和降雨的视觉效果。文章还提供了源代码示例,涵盖了从项目结构到具体实现的各个步骤。
摘要由CSDN通过智能技术生成

SwiftUI模块系列 - 已更新30篇
SwiftUI项目 - 已更新3个项目
往期Demo源码下载

技术: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部分 - 扩展一个雪花效果
其中⭐️表示添加的部分

详细

一、运行效果

请添加图片描述

二、项目结构图

在这里插入图片描述

三、⚠️程序实现 - 过程 - 基于上一篇文章【SwiftUI项目】0030、SwiftUI创建iOS15 天气应用程序滚动效果 1/2部分 - 扩展一个雪花效果

思路:

  1. 搭建主视图 - 滚动视图
  2. 搭建天气数据
  3. 自定义栈视图
  4. 使用自定义栈视图 构建天气数据 - 多个视图
  5. 使用模型构建未来十天的天气预告数据
  6. 处理上下滚动 监听偏移量 如果是负数 就处理字体和视图的可见范围
  7. ⭐️新增两个 SpriteKit的下雨文件
  8. ⭐️滚动偏移计算 雪花落地的效果进行逻辑处理
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.⭐️删除RainFallAssets资源文件 因为在项目的资源文件里面已经包含了

在这里插入图片描述

12. ⭐️创建一个文件New File 搜索Spri 选择SpriteKit Particle File类型 并且Particle Template选择为Rain 命名为RainFallLanding

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

13. ⭐️删除RainFallLandingAssets资源文件 因为在项目的资源文件里面已经包含了

在这里插入图片描述

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 - 主页

思路

  1. 搭建主视图 - 滚动视图
  2. 搭建天气数据
  3. 自定义栈视图
  4. 使用自定义栈视图 构建天气数据 - 多个视图
  5. 使用模型构建未来十天的天气预告数据
  6. 处理上下滚动 监听偏移量 如果是负数 就处理字体和视图的可见范围
  7. ⭐️新增两个 SpriteKit的下雨文件
  8. ⭐️滚动偏移计算 雪花落地的效果进行逻辑处理
//
//  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 - 自定义雪花效果
  1. 设置背景颜色透明度
  2. 修改Rain参数表
  3. 设置雪花的颜色
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

⭐️RainFallLanding - 自定义雪花落地效果

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇夜iOS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值