- 創(chuàng)建一個(gè)新的Xcode項(xiàng)目
- 選擇單視圖應(yīng)用程序,然后單擊下一步
- 為您的應(yīng)用命名,并確保用戶界面為Swift UI
- 最后摄欲,單擊“完成”
- 您新創(chuàng)建的項(xiàng)目見截圖:
這是您首次創(chuàng)建項(xiàng)目時(shí)的默認(rèn)項(xiàng)目布局。如果模擬器未顯示,請(qǐng)單擊恢復(fù)喧笔。
將ContentView文件和結(jié)構(gòu)重命名為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對(duì)WeatherApp的引用龟再。
每次進(jìn)行重大更改時(shí)书闸,預(yù)覽都會(huì)消失,只需單擊“恢復(fù)”即可再次顯示利凑。
我將逐步更易于理解的UI浆劲。這是我要執(zhí)行的操作分析:
第1步:自定義導(dǎo)航欄
- 創(chuàng)建一個(gè)名為NavBarView
- 創(chuàng)建了一個(gè)帶有系統(tǒng)名稱的圖像嫌术,這些圖標(biāo)可以通過從Apple開發(fā)人員網(wǎng)站下載SF Symbol來找到這些圖像
- 我設(shè)置resizable()修改器是為了給圖像提供一個(gè)不同的幀,然后在它之后設(shè)置resible()
- 我將標(biāo)題放置Text在2個(gè)Spacer視圖之間以使其水平居中牌借。
- 在HStackView的所有側(cè)面上添加了一個(gè)填充
- 將所有內(nèi)容括在一個(gè)VStack塊中度气,并在@State private var selected = 0上面添加 body
NavBarView.swift應(yīng)如下所示:
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結(jié)構(gòu)現(xiàn)在應(yīng)如下所示:
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)
}
}
}
現(xiàn)在,預(yù)覽應(yīng)該在屏幕中央顯示以下內(nèi)容:
第2步:模型
需要為該步驟創(chuàng)建2個(gè)文件膨报,第一個(gè)文件是一個(gè)模態(tài)文件磷籍,將保存我們的虛擬數(shù)據(jù),另一個(gè)文件是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")
]
}
}
然后創(chuàng)建一個(gè)swiftUI View文件院领,命名為MainCardView.swift。如果未顯示預(yù)覽够吩,請(qǐng)單擊“恢復(fù)”比然。
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自動(dòng)監(jiān)視該屬性的更改并更新了使用的UI部分周循。@Binding的工作方式@State與之相同强法,但它不是全局的,而是全局的鱼鼓。
- 圖像調(diào)整大小拟烫,然后將其內(nèi)容模式設(shè)置為fill
- 垂直堆疊了2個(gè)文本視圖和weatherIcon
- 將框架的minWidth和maxWidth設(shè)置為0和.infinity以使該寬度與所有設(shè)備尺寸上的屏幕寬度均匹配。
- 提示:無論屏幕大小如何迄本,都可以使用.frame(minWidth: 0 , maxWidth: .infinity)或 .frame(minHeight: 0 , maxHeight: .infinity)填充父母的寬度或高度
預(yù)覽現(xiàn)在應(yīng)該顯示以下內(nèi)容:
下一步是將我們新創(chuàng)建的視圖添加到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)
$(美元符號(hào))weather表示此本地天氣屬性的狀態(tài)綁定到MainCardView中的屬性,只要其中一個(gè)發(fā)生更改嘉赎,就會(huì)通知另一個(gè)并相應(yīng)更改置媳。簡而言之,它們是同步的公条。
其余代碼是不言自明的拇囊。
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)
}
}
預(yù)覽現(xiàn)在應(yīng)該顯示以下內(nèi)容:
步驟3:水平滾動(dòng)卡片列表
首先創(chuàng)建標(biāo)題。在下面添加以下代碼MainCardView:
Text("未來七天")
.font(.system(size: 22))
.fontWeight(.bold)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.top)
.padding(.horizontal)
上面的代碼只是創(chuàng)建一個(gè)文本靶橱,將其向左對(duì)齊并應(yīng)用水平填充寥袭。
下一步是創(chuàng)建一張卡,將其重復(fù)使用以創(chuàng)建水平滾動(dòng)列表关霸。
創(chuàng)建新的swiftUI文件传黄,創(chuàng)建方式和創(chuàng)建MainCardView一樣,對(duì)此命名為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偏移設(shè)置為30將云圖標(biāo)向下推30,這很容易解釋佳遣。
預(yù)覽現(xiàn)在應(yīng)該顯示以下內(nèi)容:
- 現(xiàn)在识埋,讓我們創(chuàng)建水平滾動(dòng)列表凡伊。返回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)
預(yù)覽應(yīng)顯示以下內(nèi)容:
步驟4:彈出詳情
當(dāng)單擊其中一張小卡片時(shí)窒舟,我們要顯示從底部開始動(dòng)畫的詳細(xì)視圖系忙。
繼續(xù)并創(chuàng)建一個(gè)名為swiftUI的新文件,DetailView.swift
用以下內(nèi)容替換文件的內(nèi)容:
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是一個(gè)容器視圖辜纲,根據(jù)其自身大小和坐標(biāo)空間定義其內(nèi)容笨觅。這意味著GeometryReader給我們尺寸和位置,我們可以使用它來動(dòng)態(tài)定位和調(diào)整子視圖的大小
我們將創(chuàng)建一個(gè)自定義形狀耕腾,用于裁剪視圖。
將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之后的父級(jí)上添加此修飾符.background(Color(self.weather.color)):
.clipShape(CustomShape(), style: FillStyle.init(eoFill: true, antialiased: true))
預(yù)覽應(yīng)顯示以下內(nèi)容:
接下來杀糯,我們將創(chuàng)建接下來的5小時(shí)視圖:
在代碼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)
}
}
和創(chuàng)建這樣的水平堆疊視圖:
將以下代碼的下方扫俺,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用來創(chuàng)建每小時(shí)的預(yù)測狼纬。
預(yù)覽應(yīng)顯示如下內(nèi)容:
現(xiàn)在讓我們將其集成DetailView到WeatherApp中:
我們想tapGesture在每個(gè)上添加一個(gè),SmallCardView以便當(dāng)我們單擊它們中的任何一個(gè)時(shí)骂际,我們都顯示相應(yīng)的詳細(xì)信息疗琉。
SmallCard(weather: weather).onTapGesture {
self.showDetails.toggle()
self.weather = weather
}
并將其添加到下面文件的頂部 weather
@State private var showDetails = false
每次SmallCard點(diǎn)擊其中一個(gè)時(shí),我們都想切換 showDetails的值歉铝。
在下面添加showDetails:
private var detailSize = CGSize(width: 0, height: UIScreen.main.bounds.height)
現(xiàn)在盈简,將新添加ScrollView的ZStack容器放入容器中,并ScrollView在ZStack塊內(nèi)部下面添加以下代碼:
DetailView(weather: self.$weather)
.offset( self.showDetails ? CGSize.zero : detailSize)
而下面這個(gè)detailSize屬性到頂部:
@State private var sampleData = Weather.sampleData
為了使DetailView動(dòng)畫效果漂亮地圍繞在這兩個(gè)調(diào)用中onTapGesture太示,withAnimation(.spring())如下所示:
withAnimation(.spring()) {
self.showDetails.toggle()
self.weather = weather
}
關(guān)閉按鈕
在中DetailView.swift柠贤,VStack用a 包圍外部,在外部ZStack下方VStack添加以下內(nèi)容
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)
重要的是這個(gè) .offset(x: 0, y: -gr.size.height / 2)类缤。在ZStack容器中添加視圖時(shí)臼勉,它們將在容器(在本例中為ZStack容器)的中心彼此堆疊。因此餐弱,要將其定位到頂部宴霸,我們將其向上移動(dòng)1/2 ZStack高度。
現(xiàn)在添加onTapGesture允許刪除DetailView膏蚓。
首先瓢谢,將其添加@Binding var showDetails: Bool
到主體上方,并showDetails: .constant(false)在DetailView_Previews塊內(nèi)的DetailView的構(gòu)造函數(shù)中添加此 參數(shù)降允。
最后恩闻,添加此修改器關(guān)閉圖標(biāo)HStatck后shadow:
.onTapGesture {
withAnimation(.spring()) {
self.showDetails.toggle()
}
}
中會(huì)出現(xiàn)錯(cuò)誤WeatherApp。只需將此參數(shù)添加,showDetails: self.$showDetails到構(gòu)造函數(shù)即可剧董。
完整DetailView.swift文件應(yīng)如下所示
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))
}
}
總結(jié)一下
- 關(guān)于項(xiàng)目的內(nèi)容,并不是文章就能給大家講明白的,想要了解更多內(nèi)容,請(qǐng)多看SwiftUI有關(guān)知識(shí)幢尚,特別是國外一些資料
- 代碼下載地址
點(diǎn)擊跳轉(zhuǎn)下載 - 歡迎點(diǎn)贊和反饋
本文為作者原著,歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明作者出處