A-A+

SwiftUI:高仿天气

2020年06月20日 iOS原创文章 暂无评论
博客主机
  • 创建一个新的Xcode项目
  • 选择单视图应用程序,然后单击下一步
  • 为您的应用命名,并确保用户界面为Swift UI
  • 最后,单击“完成”
  • 您新创建的项目见截图:
image

这是您首次创建项目时的默认项目布局。如果模拟器未显示,请单击恢复。

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()
        }
    }

更改您看到的ContentViewWeatherApp的引用。

每次进行重大更改时,预览都会消失,只需单击“恢复”即可再次显示。

我将逐步更易于理解的UI。这是我要执行的操作分析:

image
image

第1步:自定义导航栏

  1. 创建一个名为NavBarView
  2. 创建了一个带有系统名称的图像,这些图标可以通过从Apple开发人员网站下载SF Symbol来找到这些图像
  3. 我设置resizable()修改器是为了给图像提供一个不同的帧,然后在它之后设置resible()
  4. 我将标题放置Text在2个Spacer视图之间以使其水平居中。
  5. HStackView的所有侧面上添加了一个填充
  6. 将所有内容括在一个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)
        }
    }
}

现在,预览应该在屏幕中央显示以下内容:

image

第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]))
    }
}
  • @Bindingweather属性之前添加了该属性,并使用swiftUI自动监视该属性的更改并更新了使用的UI部分。@Binding的工作方式@State与之相同,但它不是全局的,而是全局的。
  • 图像调整大小,然后将其内容模式设置为fill
  • 垂直堆叠了2个文本视图和weatherIcon
  • 将框架的minWidth和maxWidth设置为0和.infinity以使该宽度与所有设备尺寸上的屏幕宽度均匹配。
  • 提示:无论屏幕大小如何,都可以使用.frame(minWidth: 0 , maxWidth: .infinity)或 .frame(minHeight: 0 , maxHeight: .infinity)填充父母的宽度或高度
image

预览现在应该显示以下内容:

下一步是将我们新创建的视图添加到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)
    }
}
image

预览现在应该显示以下内容:

步骤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,这很容易解释。

预览现在应该显示以下内容:

  1. 现在,让我们创建水平滚动列表。返回WeatherApp文件。
  2. 在接下来的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
    }
}

VStackDetailsView之后的父级上添加此修饰符.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)
    }
}

和创建这样的水平堆叠视图: 将以下代码的下方,VStackDetailView身体。

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用来创建每小时的预测。

预览应显示如下内容:

现在让我们将其集成DetailViewWeatherApp中:

我们想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动画效果漂亮地围绕在这两个调用中onTapGesturewithAnimation(.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的构造函数中添加此 参数。

最后,添加此修改器关闭图标HStatckshadow

 .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有关知识,特别是国外一些资料
  • 代码下载地址 点击跳转下载
  • 欢迎点赞和反馈 本文为作者原著,欢迎转载,转载请注明作者出处
标签:

给我留言

Copyright © ios教程,苹果粉丝,苹果资讯,ios入门教程,ios学习,ios程序员,ios视频教程,ios粉丝网 保留所有权利.   Theme  Ality

用户登录