【SwiftUI项目】0030、SwiftUI创建iOS15 天气应用程序滚动效果 1/2部分

本文档详细介绍了如何使用SwiftUI4.0和Xcode14创建一个具备滚动效果的iOS天气应用程序。内容包括项目结构、视图创建、模型构建,特别是自定义栈视图和天气数据展示,以及滚动监听和视图透明度处理。
摘要由CSDN通过智能技术生成

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

技术:SwiftUI、SwiftUI4.0、天气、天气App、天气应用程序
运行环境:
SwiftUI4.0 + Xcode14 + MacOS12.6 + iPhone Simulator iPhone 14 Pro Max

概述

使用SwiftUI创建iOS15 天气应用程序滚动效果 1/2部分

详细

一、运行效果

请添加图片描述

二、项目结构图

在这里插入图片描述

三、程序实现 - 过程

思路:

  1. 搭建主视图 - 滚动视图
  2. 搭建天气数据
  3. 自定义栈视图
  4. 使用自定义栈视图 构建天气数据 - 多个视图
  5. 使用模型构建未来十天的天气预告数据
  6. 处理上下滚动 监听偏移量 如果是负数 就处理字体和视图的可见范围
1.创建一个项目命名为 WeatherAppScrolling

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

1.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 作为模型

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

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

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. 处理上下滚动 监听偏移量 如果是负数 就处理字体和视图的可见范围
//
//  Home.swift
//  WeatherAppScrolling (iOS)
//
//  Created by 李宇鸿 on 2022/9/18.
//

import SwiftUI

struct Home: View {
    @State var offset : CGFloat = 0
    var topEdge : CGFloat
    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)
            
            
            // 主视图
            ScrollView(.vertical,showsIndicators: false){
                VStack{
                    // 天气数据……
                    VStack(alignment: .center,spacing: 5) {
                        Text("Shen Zhen")
                            .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: {
                            
                            // 内容
                            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()
                        
                    }
                   
                }
                .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
                        }
                        return Color.clear
                    }
                )
                
                
           
            }
        }
    }
    
    //
    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)
    }
}
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"),
]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

宇夜iOS

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

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

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

打赏作者

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

抵扣说明:

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

余额充值