技术:SwiftUI、SwiftUI4.0、天气、天气App、天气应用程序
运行环境:
SwiftUI4.0 + Xcode14 + MacOS12.6 + iPhone Simulator iPhone 14 Pro Max
SwiftUI创建iOS15 天气应用程序滚动效果 1/2部分
- 概述
- 详细
- 一、运行效果
- 二、项目结构图
- 三、程序实现 - 过程
- 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` 作为模型
- Code
概述
使用SwiftUI创建iOS15 天气应用程序滚动效果 1/2部分
详细
一、运行效果
二、项目结构图
三、程序实现 - 过程
思路:
- 搭建主视图 - 滚动视图
- 搭建天气数据
- 自定义栈视图
- 使用自定义栈视图 构建天气数据 - 多个视图
- 使用模型构建未来十天的天气预告数据
- 处理上下滚动
监听偏移量
如果是负数 就处理字体和视图的可见范围
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 - 主页
思路
- 搭建主视图 - 滚动视图
- 搭建天气数据
- 自定义栈视图
- 使用自定义栈视图 构建天气数据 - 多个视图
- 使用模型构建未来十天的天气预告数据
- 处理上下滚动
监听偏移量
如果是负数 就处理字体和视图的可见范围
//
// 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"),
]