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

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 - 自定义雪花落地效果

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_42816425/article/details/126810575