- 创建一个新的Xcode项目
- 选择单视图应用程序,然后单击下一步
- 为您的应用命名,并确保用户界面为Swift UI
- 最后,单击“完成”
- 您新创建的项目见截图:
这是您首次创建项目时的默认项目布局。如果模拟器未显示,请单击恢复。
将ContentView文件和结构重命名为WeatherApp,并确保在以下位置重命名其引用SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let weatherApp = WeatherApp()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = ::UIHostingController(rootView: weatherApp)::
self.window = window
window.makeKeyAndVisible()
}
}
更改您看到的ContentView对WeatherApp的引用。
每次进行重大更改时,预览都会消失,只需单击“恢复”即可再次显示。
我将逐步更易于理解的UI。这是我要执行的操作分析:
第1步:自定义导航栏
- 创建一个名为NavBarView
- 创建了一个带有系统名称的图像,这些图标可以通过从Apple开发人员网站下载SF Symbol来找到这些图像
- 我设置resizable()修改器是为了给图像提供一个不同的帧,然后在它之后设置resible()
- 我将标题放置Text在2个Spacer视图之间以使其水平居中。
- 在HStackView的所有侧面上添加了一个填充
- 将所有内容括在一个VStack块中,并在@State private var selected = 0上面添加 body
NavBarView.swift应如下所示:
import SwiftUI
struct NavBarView: View {
var country = "深圳天气"
var body: some View{
HStack {
Image(systemName: "ellipsis.circle.fill")
.resizable()
.frame(width: 25, height: 25)
Spacer()
Text(country).font(.title)
Spacer()
Image(systemName: "magnifyingglass")
.resizable()
.frame(width: 25, height: 25)
}.padding()
}
}
struct NavBarView_Previews: PreviewProvider {
static var previews: some View {
NavBarView()
}
}
WeatherApp结构现在应如下所示:
struct WeatherApp: View {
@State private var selected = 0
var body: some View {
VStack {
NavBarView(country: "深圳天气")
Picker("", selection: $selected){
Text("今天").tag(0)
Text("明天").tag(1)
}.pickerStyle(SegmentedPickerStyle() )
.padding(.horizontal)
}
}
}
现在,预览应该在屏幕中央显示以下内容:
第2步:模型
需要为该步骤创建2个文件,第一个文件是一个模态文件,将保存我们的虚拟数据,另一个文件是MainCardView。
import Foundation
struct Weather: Hashable, Identifiable {
let id: Int
let day: String
let weatherIcon: String
let currentTemp: String
let minTemp: String
let maxTemp: String
let color: String
static var sampleData: [Weather] {
return [
Weather(id: 1, day: "星期一", weatherIcon: "sun.max", currentTemp: "30", minTemp: "32", maxTemp: "29", color: "mainCard"),
Weather(id: 2, day: "星期二", weatherIcon: "sun.dust", currentTemp: "33", minTemp: "32", maxTemp: "29", color: "tuesday"),
Weather(id: 3, day: "星期三", weatherIcon: "cloud.sun.rain", currentTemp: "32", minTemp: "28", maxTemp: "29", color: "wednesday"),
Weather(id: 4, day: "星期四", weatherIcon: "cloud.sun.bolt", currentTemp: "33", minTemp: "27", maxTemp: "30", color: "thursday"),
Weather(id: 5, day: "星期五", weatherIcon: "sun.haze", currentTemp: "30", minTemp: "27", maxTemp: "29", color: "friday"),
Weather(id: 6, day: "星期六", weatherIcon: "sun.dust", currentTemp: "30", minTemp: "32", maxTemp: "34", color: "saturday"),
Weather(id: 7, day: "星期天", weatherIcon: "sun.max", currentTemp: "30", minTemp: "22", maxTemp: "32", color: "sunday")
]
}
}
然后创建一个swiftUI View文件,命名为MainCardView.swift。如果未显示预览,请单击“恢复”。
import SwiftUI
struct MainCardView: View {
@Binding var weather: Weather
var body: some View {
ZStack {
Image("card-bg")
.resizable()
.aspectRatio(contentMode: .fill)
VStack(spacing: 10) {
Text("\(weather.currentTemp)°")
.foregroundColor(Color.white)
.fontWeight(Font.Weight.heavy)
.font(Font.system(size: 70))
Image(systemName: weather.weatherIcon)
.resizable()
.foregroundColor(Color.white)
.frame(width: 100, height: 100)
.aspectRatio(contentMode: .fit)
Text("\(weather.maxTemp)°")
.foregroundColor(Color.white)
.font(.title)
.padding(.vertical)
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color(weather.color))
}
}
struct MainCardView_Previews: PreviewProvider {
static var previews: some View {
MainCardView(weather: .constant(Weather.sampleData[0]))
}
}
- @Binding在weather属性之前添加了该属性,并使用swiftUI自动监视该属性的更改并更新了使用的UI部分。@Binding的工作方式@State与之相同,但它不是全局的,而是全局的。
- 图像调整大小,然后将其内容模式设置为fill
- 垂直堆叠了2个文本视图和weatherIcon
- 将框架的minWidth和maxWidth设置为0和.infinity以使该宽度与所有设备尺寸上的屏幕宽度均匹配。
- 提示:无论屏幕大小如何,都可以使用.frame(minWidth: 0 , maxWidth: .infinity)或 .frame(minHeight: 0 , maxHeight: .infinity)填充父母的宽度或高度
预览现在应该显示以下内容:
下一步是将我们新创建的视图添加到WeatherApp视图中。
将其添加到selected属性下面:
@State private var weather = Weather(id: 1, day: "星期一", weatherIcon: "sun.max", currentTemp: "29", minTemp:"31", maxTemp: "33", color: "mainCard")
MainCardView(weather: $weather)
.cornerRadius(CGFloat(20))
.padding()
.shadow(color: Color(self.weather.color)
.opacity(0.4), radius: 20, x: 0, y: 20)
$(美元符号)weather表示此本地天气属性的状态绑定到MainCardView中的属性,只要其中一个发生更改,就会通知另一个并相应更改。简而言之,它们是同步的。
其余代码是不言自明的。
ZStack {
ScrollView(.vertical, showsIndicators: false) {
MainCardView(weather: $weather)
.cornerRadius(CGFloat(20))
.padding()
.shadow(color: Color(self.weather.color)
.opacity(0.4), radius: 20, x: 0, y: 20)
}
}
预览现在应该显示以下内容:
步骤3:水平滚动卡片列表
首先创建标题。在下面添加以下代码MainCardView:
Text("未来七天")
.font(.system(size: 22))
.fontWeight(.bold)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.top)
.padding(.horizontal)
上面的代码只是创建一个文本,将其向左对齐并应用水平填充。
下一步是创建一张卡,将其重复使用以创建水平滚动列表。
创建新的swiftUI文件,创建方式和创建MainCardView一样,对此命名为SmallCard
import SwiftUI
struct SmallCard: View {
var weather: Weather = Weather(id: 1, day: "星期一", weatherIcon: "sun.max", currentTemp: "30", minTemp: "32", maxTemp: "29", color: "mainCard")
var body: some View {
VStack(spacing: 20) {
Text(self.weather.day).fontWeight(.bold)
.foregroundColor(Color.white)
Image(systemName: self.weather.weatherIcon)
.resizable()
.foregroundColor(Color.white)
.frame(width: 60, height: 60)
ZStack {
Image("cloud")
.resizable()
.scaledToFill()
.offset(CGSize(width: 0, height: 30))
VStack(spacing: 8) {
Text("\(self.weather.currentTemp)°").font(.title).foregroundColor(Color.white).fontWeight(.bold)
HStack {
Text("\(self.weather.minTemp)°").foregroundColor(Color("light-text"))
Text("\(self.weather.maxTemp)°").foregroundColor(Color.white)
}
}
}
}
.frame(width: 200, height: 300)
.background(Color(self.weather.color))
.cornerRadius(30)
.shadow(color: Color(weather.color).opacity(0.7), radius: 10, x: 0, y: 8)
}
}
struct SmallCard_Previews: PreviewProvider {
static var previews: some View {
SmallCard()
}
}
代码相似。唯一的新事物是ZStack容器。ZStack是将其子视图彼此叠加的视图。(“ Z”代表在3D空间中基于深度的Z轴)
我通过将其y偏移设置为30将云图标向下推30,这很容易解释。
预览现在应该显示以下内容:
- 现在,让我们创建水平滚动列表。返回WeatherApp文件。
- 在接下来的7天文本下方添加以下代码:
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(Weather.sampleData, id: \.id) { weather in
SmallCard(weather: weather).onTapGesture {
withAnimation(.spring()) {
self.showDetails.toggle()
self.weather = weather
}
}
}
}.frame( height: 340)
.padding(.horizontal)
}.frame( height: 350, alignment: .top)
预览应显示以下内容:
步骤4:弹出详情
当单击其中一张小卡片时,我们要显示从底部开始动画的详细视图。
继续并创建一个名为swiftUI的新文件,DetailView.swift
用以下内容替换文件的内容:
import SwiftUI
struct DetailView: View {
@Binding var weather: Weather
var body: some View {
GeometryReader { gr in
VStack(spacing: 20) {
// Day text
Text(self.weather.day).fontWeight(.bold)
.font(.system(size: 60))
.frame(height: gr.size.height * 1/10)
.minimumScaleFactor(0.5)
.foregroundColor(Color.white)
// Weather image
Image(systemName: self.weather.weatherIcon)
.resizable()
.foregroundColor(Color.white)
.frame(width: gr.size.height * 3 / 10, height: gr.size.height * 3 / 10)
// Degrees texts
VStack {
VStack(spacing: 20) {
Text("\(self.weather.currentTemp)°")
.font(.system(size: 50))
.foregroundColor(Color.white)
.fontWeight(.bold)
.frame(height: gr.size.height * 0.7/10)
.minimumScaleFactor(0.5)
HStack(spacing: 40) {
Text("\(self.weather.minTemp)°")
.foregroundColor(Color("light-text"))
.font(.title)
.minimumScaleFactor(0.5)
Text("\(self.weather.maxTemp)°")
.foregroundColor(Color.white)
.font(.title)
.minimumScaleFactor(0.5)
}
}
}
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: gr.size.height, alignment: .bottom)
.background(Color(self.weather.color))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(weather: .constant(Weather(id: 1, day: "星期一", weatherIcon: "sun.max", currentTemp: "24", minTemp: "25", maxTemp: "29", color: "mainCard")), showDetails: .constant(false))
}
}
注意:GeometryReader是一个容器视图,根据其自身大小和坐标空间定义其内容。这意味着GeometryReader给我们尺寸和位置,我们可以使用它来动态定位和调整子视图的大小
我们将创建一个自定义形状,用于裁剪视图。
将DetailsView其添加到其块的下方和外部:
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
let cornerRadius:CGFloat = 40
var path = Path()
path.move(to: CGPoint(x: 0, y: cornerRadius))
path.addQuadCurve(to: CGPoint(x: cornerRadius, y: 0), control: CGPoint.zero)
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: 0))
path.addQuadCurve(to: CGPoint(x: rect.width, y: cornerRadius), control: CGPoint(x: rect.width , y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.closeSubpath()
return path
}
}
并VStack在DetailsView之后的父级上添加此修饰符.background(Color(self.weather.color)):
.clipShape(CustomShape(), style: FillStyle.init(eoFill: true, antialiased: true))
预览应显示以下内容:
接下来,我们将创建接下来的5小时视图:
在代码CustomShape块下方添加以下代码:
struct HourView: View {
var hour = "14:00"
var icon = "sun.max.fill"
var color = "wednesday"
var body: some View {
GeometryReader { gr in
VStack{
Text(self.hour).foregroundColor(Color("text"))
Image(systemName: self.icon)
.resizable()
.foregroundColor(Color(self.color))
.frame(width: gr.size.height * 1/3, height: gr.size.height * 1/3)
Text("24°")
.font(.system(size: 24))
.foregroundColor(Color("text"))
.fontWeight(.semibold)
}
}.padding(.vertical, 30)
}
}
和创建这样的水平堆叠视图:
将以下代码的下方,VStack在DetailView身体。
HStack (spacing: 20){
HourView()
HourView(hour: "15:00", icon: "sun.dust.fill", color: "tuesday")
HourView(hour: "16:00",icon: "cloud.rain.fill", color: "thursday")
HourView(hour: "17:00",icon: "cloud.bolt.fill", color: "sunday")
HourView(hour: "18:00",icon: "snow", color: "mainCard")
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: gr.size.height * 2 / 10)
.padding(.horizontal)
.background(Color.white)
.cornerRadius(30)
.padding()
如您所见,我正在重HourView用来创建每小时的预测。
预览应显示如下内容:
现在让我们将其集成DetailView到WeatherApp中:
我们想tapGesture在每个上添加一个,SmallCardView以便当我们单击它们中的任何一个时,我们都显示相应的详细信息。
SmallCard(weather: weather).onTapGesture {
self.showDetails.toggle()
self.weather = weather
}
并将其添加到下面文件的顶部 weather
@State private var showDetails = false
每次SmallCard点击其中一个时,我们都想切换 showDetails的值。
在下面添加showDetails:
private var detailSize = CGSize(width: 0, height: UIScreen.main.bounds.height)
现在,将新添加ScrollView的ZStack容器放入容器中,并ScrollView在ZStack块内部下面添加以下代码:
DetailView(weather: self.$weather)
.offset( self.showDetails ? CGSize.zero : detailSize)
而下面这个detailSize属性到顶部:
@State private var sampleData = Weather.sampleData
为了使DetailView动画效果漂亮地围绕在这两个调用中onTapGesture,withAnimation(.spring())如下所示:
withAnimation(.spring()) {
self.showDetails.toggle()
self.weather = weather
}
关闭按钮
在中DetailView.swift,VStack用a 包围外部,在外部ZStack下方VStack添加以下内容
HStack {
Image(systemName: "xmark")
.resizable()
.foregroundColor(Color.red)
.frame(width: 20, height: 20)
}.padding(20
).background(Color.white)
.cornerRadius(100)
.offset(x: 0, y: -gr.size.height / 2)
.shadow(radius: 20)
重要的是这个 .offset(x: 0, y: -gr.size.height / 2)。在ZStack容器中添加视图时,它们将在容器(在本例中为ZStack容器)的中心彼此堆叠。因此,要将其定位到顶部,我们将其向上移动1/2 ZStack高度。
现在添加onTapGesture允许删除DetailView。
首先,将其添加@Binding var showDetails: Bool
到主体上方,并showDetails: .constant(false)在DetailView_Previews块内的DetailView的构造函数中添加此 参数。
最后,添加此修改器关闭图标HStatck后shadow:
.onTapGesture {
withAnimation(.spring()) {
self.showDetails.toggle()
}
}
中会出现错误WeatherApp。只需将此参数添加,showDetails: self.$showDetails到构造函数即可。
完整DetailView.swift文件应如下所示
import SwiftUI
struct DetailView: View {
@Binding var weather: Weather
@Binding var showDetails: Bool
var body: some View {
GeometryReader { gr in
ZStack {
VStack(spacing: 20) {
// Day text
Text(self.weather.day).fontWeight(.bold)
.font(.system(size: 60))
.frame(height: gr.size.height * 1/10)
.minimumScaleFactor(0.5)
.foregroundColor(Color.white)
// Weather image
Image(systemName: self.weather.weatherIcon)
.resizable()
.foregroundColor(Color.white)
.frame(width: gr.size.height * 3 / 10, height: gr.size.height * 3 / 10)
// Degrees texts
VStack {
VStack(spacing: 20) {
Text("\(self.weather.currentTemp)°")
.font(.system(size: 50))
.foregroundColor(Color.white)
.fontWeight(.bold)
.frame(height: gr.size.height * 0.7/10)
.minimumScaleFactor(0.5)
HStack(spacing: 40) {
Text("\(self.weather.minTemp)°")
.foregroundColor(Color("light-text"))
.font(.title)
.minimumScaleFactor(0.5)
Text("\(self.weather.maxTemp)°")
.foregroundColor(Color.white)
.font(.title)
.minimumScaleFactor(0.5)
}
}
}
// Hourly views
HStack (spacing: 20){
HourView()
HourView(hour: "15:00", icon: "sun.dust.fill", color: "tuesday")
HourView(hour: "16:00",icon: "cloud.rain.fill", color: "thursday")
HourView(hour: "17:00",icon: "cloud.bolt.fill", color: "sunday")
HourView(hour: "18:00",icon: "snow", color: "mainCard")
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: gr.size.height * 2 / 10)
.padding(.horizontal)
.background(Color.white)
.cornerRadius(30)
.padding()
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: gr.size.height, alignment: .bottom)
.background(Color(self.weather.color))
.clipShape(CustomShape(), style: FillStyle.init(eoFill: true, antialiased: true))
// Close icon
HStack {
Image(systemName: "xmark")
.resizable()
.foregroundColor(Color.red)
.frame(width: 20, height: 20)
}.padding(20
).background(Color.white)
.cornerRadius(100)
.offset(x: 0, y: -gr.size.height / 2)
.shadow(radius: 20)
.onTapGesture {
withAnimation(.spring()) {
self.showDetails.toggle()
}
}
}
}
}
}
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
let cornerRadius:CGFloat = 40
var path = Path()
path.move(to: CGPoint(x: 0, y: cornerRadius))
path.addQuadCurve(to: CGPoint(x: cornerRadius, y: 0), control: CGPoint.zero)
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: 0))
path.addQuadCurve(to: CGPoint(x: rect.width, y: cornerRadius), control: CGPoint(x: rect.width , y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.closeSubpath()
return path
}
}
struct HourView: View {
var hour = "14:00"
var icon = "sun.max.fill"
var color = "wednesday"
var body: some View {
GeometryReader { gr in
VStack{
Text(self.hour).foregroundColor(Color("text"))
Image(systemName: self.icon)
.resizable()
.foregroundColor(Color(self.color))
.frame(width: gr.size.height * 1/3, height: gr.size.height * 1/3)
Text("24°")
.font(.system(size: 24))
.foregroundColor(Color("text"))
.fontWeight(.semibold)
}
}.padding(.vertical, 30)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(weather: .constant(Weather(id: 1, day: "星期一", weatherIcon: "sun.max", currentTemp: "24", minTemp: "25", maxTemp: "29", color: "mainCard")), showDetails: .constant(false))
}
}
总结一下
- 关于项目的内容,并不是文章就能给大家讲明白的,想要了解更多内容,请多看SwiftUI有关知识,特别是国外一些资料
- 代码下载地址
点击跳转下载 - 欢迎点赞和反馈
本文为作者原著,欢迎转载,转载请注明作者出处