כאשר אפל הכריזה על SwiftUI ב-WWDC 2019והנה לך! זה עתה יצרת את האפליקציה הראשונה שלך עם SwiftUI.

הדרכה שנכתבה על ידיבנג'מין פיזאנומהאולפן הצרפתילונבי
הערה: כדי לעקוב אחר הדרכה זו ישירות ב-Xcode, אתה יכולהעלה את הפרויקט ל- Github.
L'API
ואחרי?ממשקי API חינמיים רבים המספקים נתוני מזג אווירהפרויקט המלא ב-Github שלישמיים אפלים@benjamin_pisanoצור את החשבון שלך כאןאַריָה

, מיד רציתי להתחיל בצפייה במפגשים ובמעקב אחר הדרכות באינטרנט. רציתי ללכת רחוק יותר מאפליקציית הדגמה כדי לראות מה SwiftUI יכול להציע. אז החלטתי לכתוב אפליקציית מזג אוויר באמצעות המסגרת הזו באופן בלעדי. הנה איך עשיתי את זה.לבדוק את תפקודו התקיןקיים
תלתל https://api.darksky.net/forecast/your_key/45.572353,5.915807
. אני בחרתילהחזיר קובץ JSON המכיל נתוני מזג אווירעל נוחות השימוש בו. תצטרך ליצור חשבון כדי ליצור את מפתח ה-API שלך ולהפעיל את האפליקציה הזו. אתה יכולצ'מבריוהפק את המפתח שלך בחינם.
Xcode
כדי להשתמש ב- SwiftUI באפליקציה שלך, עליך להוריד את Xcode 11 בטא באתר Apple Developer בקטע ההורדותלִשְׁפּוֹך

, הקלד במסוף שלך:צור את הפרויקט שלך עם SwiftUIהפקודה הזו צריכה
פתח את Xcodeמהעיר שלצור פרויקט חדש
. כעת אתה מוכן להגדיר את פרויקט Xcode.הַבָּא
. תכנן חיבור אינטרנט טוב מכיוון ש-Xcode 11 שוקל קצת יותר מ-5GB.

לוח הבקרה של הפרויקט שלך.
לאחר השלמת ההורדה, תוכל סוף סוף להתחילמוכן לקוד. אתה חסר סבלנות? גם אני! 😃
צור את התבנית
דיווח מזג האוויר
, לחץבואו ניצור את המודל שלנו אשר יאחזר את הנתונים הללו(או עבור אל קובץ > חדש > פרויקט...). בחר אפליקציית תצוגה יחידה. תן שם לפרויקט שלך "מזג אוויר" וודא שהתיבה של SwiftUI מסומנת. לחץ על
כדי ליצור את הפרויקט.
מזג אוויר struct: Codable {
var current: HourlyWeather
var hours: Weather.List<HourlyWeather>
var week: Weather.List<DailyWeather>
enum CodingKeys: String, CodingKey {
מקרה נוכחי = "נכון לעכשיו"
שעות מקרה = "לשעה"
שבוע מקרה = "יומי"
}
}
Weather.Swift
הרחבה מזג אוויר {
רשימת מבנים<T: Codable & Identifiable> : ניתן לקידוד {
היה ערמומי:
enum CodingKeys: String, CodingKey {
רשימת מקרים = "נתונים"
}
}
}
WeatherList.swift
struct DailyWeather: ניתן לקידוד, ניתן לזיהוי {
var id: נתונים {
זמן החזרה
}
היה שעה: תאריך
var maxTemperature: כפול
var minTemperatur: כפול
סמל var: Weather.Icon
enum CodingKeys: String, CodingKey {
זמן מקרה = "זמן"
case maxTemperature = "טמפרטורה גבוהה"
מקרה minTemperature = "טמפרטורה נמוכה"
סמל מקרה = "סמל"
}
}
DailyWeather.swift
struct HourlyWeather: ניתן לקידוד, לזיהוי {
var id: נתונים {
זמן החזרה
}
היה שעה: תאריך
הייתה טמפרטורה: כפולה
סמל var: Weather.Icon
}
HourlyWeather.swift
הרחבה מזג אוויר {
סמל enum: מחרוזת, ניתן לקידוד {
מקרה clearDay = "יום בהיר"
מקרה clearNight = "בהיר-לילה"
מקרה גשם = "גשם"
מקרה שלג = "שלג"
case sleet = "סleet"
מקרה רוח = "רוח"
ערפל מקרה = "ערפל"
מקרה מעונן = "מעונן"
case partyCloudyDay = "יום מעונן חלקית"
case partyCloudyNight = "לילה מעונן חלקית"
var image: תמונה {
להחליף עצמי {
case .clearDay:
return Image(systemName: "sun.max.fill")
case .clearNight:
return Image(systemName: "moon.stars.fill")
מקרה .rain:
return Image(systemName: "cloud.rain.fill")
case .snow:
החזר תמונה (שם מערכת: "שלג")
case .sleet:
return Image(systemName: "cloud.sleet.fill")
case .wind:
החזר תמונה (שם מערכת: "רוח")
מקרה .fog:
return Image(systemName: "cloud.fog.fill")
case .cloudy:
return Image(systemName: "cloud.fill")
case .partyCloudyDay:
return Image(systemName: "cloud.sun.fill")
case .partyCloudyNight:
return Image(systemName: "cloud.moon.fill")
}
}
}
}
WeatherIcon.swift
אנחנו עכשיוצור WeatherManager שיכלול את מפתח ה-Dark Sky API שלך.
class WeatherManager {
מפתח let static: String = "" // הזן את מפתח ה-API של DarkSky כאן
static let baseURL: String = "https://api.darksky.net/forecast/\(key)/"
}
WeatherManager.swift
הדגם שלנו די פשוטעכשיו כשאתה יודע את כתובת האתר לאחזור נתוני מזג אוויר עבור עיר,בואו ניצור את המודל שלנו לכל עיר.
ערים
דגם העיר קצת יותר מענייןהדגם שלנו צריך להיראות כך:כְּרִיכָה
אתה יכול גם

רגע... מה? כְּרִיכָה?
מושג הכריכה הוצג לראשונה ב-Mac OS X ב-Interface Builder.אין נציג, אין מטפל בהשלמות: הנה הכריכה. הוא תואם לפרוטוקול Codable המאפשר לנו להמיר בקלות את ה-JSON המוחזר על ידי ה-API לאובייקט באפליקציה שלנו. זה דגם מאוד קלאסי, אז לא אסביר אותו בפירוט. עַכשָׁיו,נעדר לחלוטין ב-iOS.

כריכה עם Storyboard ב-macOS.
הערה: מושג הכריכה אינו ספציפי לסוויפט או ל-Objective-C.
כי הוא מציג את המושג שלעם Combine ו-SwiftUI, אפל מביאה כריכה מעודכנת.
. זה איפשר לך לצפות במשתנה ולעדכן את חלק ממשק המשתמש של האפליקציה באופן אוטומטי בהתבסס על שינויים במודל.בואו ניצור את אובייקט העיר שלנו שיכיל את שם העיר. ב-Mac OS X, לא היה קל מאוד להשתמש בכריכה. זה היה אפילו יותר גרוע לנפות באגים עם זה. עם הזמן, אפל חזרה לדגם עם נציגים למרכיביו ולא מחייבים, בעיקר כדי לצמצם את הפער שהולך וגדל בין AppKit ל- UIKit. האחרונים לא השתמשו בכריכה, מושג
ייבוא SwiftUI
ייבוא קומבינה
class City: BindableObject {
var didChange = PassthroughSubject<City, Never> ()
שם var: מחרוזת
מזג אוויר var: מזג אוויר? {
didSet {
didChange.send(self)
}
}
init(שם: מחרוזת) {
self.name = שם
self.getWeather()
}
private func getWeather() {
guard let url = URL(string: WeatherManager.baseURL + "45.572353,5.915807?units=ca&lang=fr") else {
לַחֲזוֹר
}
URLSession.shared.dataTask(with: url) { (נתונים, תגובה, שגיאה) ב
שומר תן נתונים = נתונים אחרים {
לַחֲזוֹר
}
לעשות {
תן מפענח = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let weatherObject = try decoder.decode(Weather.self, from: data)
DispatchQueue.main.async {
self.weather = weatherObject
}
} לתפוס {
print(error.localizedDescription)
}
}.קוֹרוֹת חַיִים()
}
}
City.swift
קוד זה מכיל מספר מושגים חדשים.
הַיוֹם,לראות שהחפץ של העירייה עומד בפרוטוקולBindableObject
, בצורה הרבה יותר אלגנטית. בואו לגלות זאת על ידי יצירת המודל שלנו לערים.לְשַׁלֵב
קוֹדֶם כֹּל,BindableObject
ונתוני מזג האוויר שלו. האובייקט שלנו צריך להיראות כך:לבחון כאשר המאפיינים שלו משתנים. לא להיבהל! נסביר אותם צעד אחר צעד.didChange.send(self)ראשית, אתה יכולהמשמעות היא שבכל פעם שמשתנה מזג האוויר משתנה, צופים באובייקט העיר יקבלו הודעהשהוא פרוטוקול חדש שהוצג עם
הערה: כאן, אני לא קורא ל-didChange.send(self) במגדיר של משתנה השם, כי נניח ששמה של עיר לעולם לא משתנה לאחר אתחולה.
.getWeather()מאפשר את החפץ שלנואחזר נתוני מזג אוויר בעיר באופן אסינכרוני. זה מצריך קריאה לפונקציה במגדיר של המשתנים שלנו שיש להקפיד עליהם.
הערה חשובה: לפי התיעוד של אפל, יש לקרוא ל-didChange.send(self) רק בשרשור הראשי. זו הסיבה שאני משתמש ב-DispatchQueue.main כדי לשנות את משתנה מזג האוויר מכיוון ש-URLSession פועל ברקע.
צריך להתקשר כאשר ברצונך להודיע שאובייקט זה עבר שינוי. אתה יכול לראות כאן שאנחנו קוראים לפונקציה הזו במגדיר של משתנה מזג האוויר.צור את האובייקט הניתן לאגד שלךשחל שינוי באובייקט הזה ושכנראה צריך לעדכן את הממשק.

הפונקציהבואו ניצור חנות CityStore שתכיל את רשימת הערים שלנוva
ייבוא SwiftUI
ייבוא קומבינה
class CityStore: BindableObject {
let didChange = PassthroughSubject<CityStore, Never> ()
var cities: [City] = [City(שם: "Chambery")] {
didSet {
didChange.send(self)
}
}
}
ברגע שהעיר מאותחלת. כאשר הנתונים מאוחזרים, הוא מפענח את ה-JSON ומשנה את משתנה מזג האוויר.

אתה פשוט
. עבודה יפה.הפרוטוקולBindableObject
מאפשר לנו לצפות בשינוייםעַכשָׁיו,
. CityStore חייב להיות גם ניתן לאגד כדי להיות מסוגל לראות אם עיר נוספה או נמחקה. אבל עכשיו כשאתה מבין את עקרון הכריכה, זה הולך להיות סופר פשוט, נכון?יצרנו אובייקטעִיר
CityStore.swift
בואו ניקח הפסקה מהירה כדי לסכם את מה שראינו זה עתה.יצרנו חנות CityStore שתואמת את הפרוטוקולBindableObject
•
ממשק משתמש
אם מעולם לא ראיתם איך נראה ממשק שנעשה עם SwiftUI, אני מזמין אתכם לצפות במדריכים של אפל שהם מצוינים להיכרות עם עיצוב ממשק.
יצירת ממשק משתמש עם SwiftUI היא פשוטה מאוד ומהנה למדישל אובייקט על ידי קריאה ל- didChange.send(self) במגדיר המשתנים שלו.SwiftUI מאפשר יותר איטרציות, שלדעתי יתרון אמיתי•
Storyboard כבר לא קיים, מאז SwiftUI יוצר, חיהתואם את פרוטוקול BindableObject לצפייה בשינויים בעיר (כאשר מאחזרים נתוני מזג אוויר).

הקוד והתצוגה המקדימה. (הערה: תזדקק ל-macOS 10.15 Catalina לתצוגה מקדימה)
•בואו ניצור את הנוף שלנו שיכיל את הערים שלנוכדי לצפות בשינויים ברשימת הערים שלנו (כאשר המשתמש מוסיף או מסיר עיר).
ייבוא SwiftUI
struct CityListView : הצג {
@EnvironmentObject var cityStore: CityStore
@State var isAddingCity: Bool = false
@State private var isEditing: Bool = false
var body: some View {
NavigationView {
רשימת {
Section(header: Text("הערים שלך")) {
ForEach(cityStore.cities) { עיר ב
CityRow(עיר: עיר)
}
.onDelete(ביצוע: מחק)
.onMove(ביצוע: העבר)
}
}
.navigationBarItems(מוביל: EditButton(), נגרר: addButton)
.navigationBarTitle(Text("מזג אוויר"))
}
}
private var addButton: some View {
Button(פעולה: {
self.isAddingCity = true
self.isEditing = false
}) {
Image(systemName: "plus.circle.fill")
.font(.title)
}
.presentation(isAddingCity ? newCityView: אפס)
}
מחיקה פרטית (בהקזות: IndexSet) {
עבור אינדקס בקיזוז {
cityStore.cities.remove(בכתובת: index)
}
}
פרטי func move (ממקור: IndexSet, ליעד: Int) {
var removeCities: [עיר] =
עבור אינדקס במקור {
removeCities.append(cityStore.cities[index])
cityStore.cities.remove(בכתובת: index)
}
cityStore.cities.insert(contentsOf: removeCities, ב: יעד)
}
private var newCityView: Modal {
Modal(NewCityView(isAddingCity: $isAddingCity).environmentObject(cityStore)) {
self.isAddingCity = false
}
}
}
CityListView.swift. תמיד השתמשתי ב- Storyboard כדי לעצב את הממשקים שלי, אבל SwiftUI הציב את הרף גבוה מאוד. על ידי צמצום משמעותי של הקוד הדרוש לעיצוב ממשק,
הקוד הזה אמור להיות די קל להבנה
הערה: Swift 5.1 מציגה תכונה חדשה בשם Property Wrappers. אני ממליץ על מאמר זה שמסביר את המושג הזה בפירוט קטן יותר.
var body: some Viewלמפתחים ומעצבים.
משתנה זה מייצג את גוף ההשקפה שלנו
, סקירה כללית של האפליקציה שלך. אם הקוד שלך משתנה, התצוגה המקדימה תתעדכן. אבל אפילו יותר חזק: אם תשנה את התצוגה המקדימה, גם הקוד שלך יתעדכן. זה כמעט קסום.
עַכשָׁיו,
@כְּרִיכָה.
@Binding משמש לקשירת משתנה @State
@ObjectBinding
, אבל יש כמה מילות מפתח חדשות. שוב, בואו נסביר אותם צעד אחר צעד.@ObjectBinding משמש כדי לצפות בשינויים באובייקט שמגיב לפרוטוקול BindableObject שראינו בעבר.. זה המקום שבו תבנה את הממשק שלך.
@EnvironmentObject מאפשר לנו להשתמש במופע בודד של אובייקט שישמש באופן גלובלי באפליקציה. מעשי לאובייקטים כמו משתמש, המשמש בכל מקום באפליקציה.לעדכן את הממשק שלנו בהתאם למודל שלנו@State הוא גלישת נכסים המייצגת מצב בתצוגה. יש להשתמש ב-@State רק בתצוגה ולהכריז על פרטיות כדי להימנע משימוש בו בכל מקום. לדוגמה, ב-CityListView שלנו, יש מצב שמגדיר האם המשתמש עורך את רשימת העיר.
SceneDelegate.swift
ייבוא UIKit
ייבוא SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
חלון var: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// השתמש ב-UIHostingController כבקר תצוגת שורש של חלונות
let window = UIWindow(frame: UIScreen.main.bounds)
תן cityStore = CityStore()
window.rootViewController = UIHostingController(rootView: CityListView().environmentObject(cityStore))
self.window = חלון
window.makeKeyAndVisible()
}
}
. במקרים מסוימים, תצטרך להעביר מצב לתצוגת צאצא של ההיררכיה שלך. לאחר מכן תוכל להשתמש ב-@Binding בתצוגת הילד שלך ולהעביר אליו את המשתנה @State שלך.SceneDelegate.swiftמאפיינים אלו יאפשרו לנו
.
בואו ניצור חנות CityStore ונעביר אותה כ- EnvironmentObject
ייבוא SwiftUI
struct CityView : View {
@ObjectBinding var city = City(שם: "Chambéry")
var body: some View {
רשימת {
Section(header: Text("Now")) {
CityHeaderView(עיר: עיר)
}
Section(header: Text("Hourly")) {
CityHourlyView(עיר: עיר)
}
Section(header: Text("השבוע")) {
ForEach(city.weather?.week.list ??
) { יום ב
CityDailyView (יום: יום)
}
}
}
.navigationBarTitle(Text(city.name))
}
כדי להפוך את CityListView לתצוגה הראשונה שתוצג באפליקציה שלנו, שנה את הקובץCityView.swiftכָּזֶה:
הערה: אנו משתמשים ב-@ObjectBinding ב-CityView שלנו. זה מאפשר למשתמש לגשת לתצוגה זו גם אם נתוני מזג האוויר עדיין לא אוחזרו. לאחר הטעינה, כל רכיבי הממשק המכילים הפניה לאובייקט זה יעובדו מחדש על המסך. זה עובד רק אם האובייקט שלך תואם את פרוטוקול BindableObject.
כל התצוגות של נתוני מזג האוויר
struct CityHeaderView: הצג {
@ObjectBinding var city: עיר
טמפרטורת var: מחרוזת {
שומר תן טמפרטורה = city.weather?.current.temperature else {
החזר "-ºC"
}
return temperature.formattedTemperature
}
var body: some View {
HStack(יישור: .center) {
Spacer()
HStack(יישור: .center, מרווח: 16) {
city.weather?.current.icon.image
.font(.largeTitle)
טקסט (טמפרטורה)
.font(.largeTitle)
}
Spacer()
}
.frame(גובה: 110)
}
}
CityHeaderView.swift
struct CityHourlyView : הצג {
@ObjectBinding var city: עיר
private let rowHeight: CGFloat = 110
var body: some View {
ScrollView(alwaysBounceHorizontal: true, showsHorizontalIndicator: false) {
HStack(רווח: 16) {
ForEach(city.weather?.hours.list ??
) { שעה ב
VStack(רווח: 16) {
טקסט (hour.time.formattedHour)
.font(.footnote)
hour.icon.image
.font(.body)
טקסט (שעה.טמפרטורה.פורמטטמפרטורת)
.font(.headline)
}
}
}
.frame(height: rowHeight)
.padding(.trailing)
}
.listRowInsets(EdgeInsets(top: 0, lead: 0, bottom: 0, trailing: 0))
.frame(height: rowHeight)
}
}
CityHourlyView.swift
struct CityDailyView : הצג {
@State var day: DailyWeather
var body: some View {
ZStack {
HStack(יישור: .center) {
טקסט(day.time.formattedDay)
Spacer()
HStack(רווח: 16) {
verticalTemperatureView(דקה: אמיתי)
verticalTemperatureView(דקה: false)
}
}
HStack(יישור: .center) {
Spacer()
day.icon.image
.font(.body)
Spacer()
}
}
}
func verticalTemperatureView(דקה: Bool) -> תצוגה מסוימת {
VStack(alignment: .trailing) {
Text(min ? "min" : "max")
.font(.footnote)
.color(.grey)
טקסט (מינימום ? day.minTemperature.formattedTemperature : day.maxTemperature.formattedTemperature)
.font(.headline)
הנה אנחנו
ל- CityListView שלנו.}כעת נוכל ליצור תצוגת מזג אוויר עבור ערים:
}
הערה: CollectionView עדיין אינו רכיב זמין ב- SwiftUI. כדי לפצות על היעדרות זו, כאן אנו משתמשים ב- ScrollView המכיל HStack עבור תצוגת הזמן.
האם נוכל ליצור תצוגה להוספת עיר חדשה
ייבוא SwiftUI
ייבוא קומבינה
ייבוא MapKit
class CityFinder: NSObject, BindableObject {
var didChange = PassthroughSubject<CityFinder, Never> ()
תוצאות var: [מחרוזת] =
{
didSet {
didChange.send(self)
}
}
מחפש פרטי var: MKLocalSearchCompleter
override init() {
תוצאות =
searcher = MKLocalSearchCompleter()
super.init()
searcher.resultTypes = .address
searcher.delegate = עצמי
}
func search(_ text: String) {
searcher.queryFragment = טקסט
}
}
הרחבה CityFinder: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {עַכשָׁיו,
:
results = completer.results.map({ $0.title })
}
}
CityFinder.swift
נשתמש ב-MapKit כדי להציע לנו ערים
struct NewCityView : הצג {
@Binding var isAddingCity: Bool
@State private var search: String = ""
@ObjectBinding var cityFinder: CityFinder = CityFinder()
@EnvironmentObject var cityStore: CityStore
var body: some View {
NavigationView {
רשימת {
סעיף {
TextField($search, placeholder: Text("Search City")) {
self.cityFinder.search(self.search)
}
}
סעיף {
ForEach(cityFinder.results.identified(by: \.self)) { result in
Button(פעולה: {
self.addCity(מ: תוצאה)
self.isAddingCity = false
}) {
טקסט (תוצאה)
}
.foregroundColor(.black)
}
}
}
.navigationBarTitle(Text("הוסף עיר"))
.navigationBarItems(מוביל: cancelButton)
.listStyle(.grouped)
}
}
private var cancelButton: some View {
Button(פעולה: {
self.isAddingCity = false
}) {
טקסט ("ביטול")
}
}
private func addCity(מתוך תוצאה: מחרוזת) {
let cityName = result.split(מפריד: ",").first ?? ""
CityDailyView.swift
לבסוף, אנחנוlet city = City(name: String(cityName)). בואו נבנה את המודל שלנו שיציע ערים על סמך חיפוש. שוב, מודל זה יהיה ניתן לאגד כך שתוכל לצפות בתוצאות באופן אסינכרוני.
.cityStore.cities.append(city)עכשיו בואו נבנה את התצוגה המודאלית שלנו כדי להוסיף עיר:
}
}
השקפה זו מעניינת משתי סיבות:NewCityView.swift1)היא מראה לנו כיצד להציג תצוגה מודאליתעם SwiftUI. לשם כך תצטרכו להעביר משתנה שמציין האם יש להציג את התצוגה או לא. נכון לעכשיו, הפעולה הזו קצת בעייתית.@EnvironmentObject שוב מאפשר לנו להשתמש במופע בודד2)