美文网首页iOS分享世界
SwiftUI:仿写天气

SwiftUI:仿写天气

作者: 醉看红尘这场梦 | 来源:发表于2020-06-16 17:11 被阅读0次
    • 创建一个新的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,这很容易解释。

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


    image
    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)
    

    预览应显示以下内容:

    image

    步骤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))
    

    预览应显示以下内容:

    image

    接下来,我们将创建接下来的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用来创建每小时的预测。

    预览应显示如下内容:

    image

    现在让我们将其集成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有关知识,特别是国外一些资料
    • 代码下载地址
      点击跳转下载
    • 欢迎点赞和反馈
      本文为作者原著,欢迎转载,转载请注明作者出处

    相关文章

      网友评论

        本文标题:SwiftUI:仿写天气

        本文链接:https://www.haomeiwen.com/subject/ynurxktx.html