Всем привет!

В этом посте я расскажу вам шаг за шагом по созданию приложения WatchOS с использованием SwiftUI и пары библиотек для обработки сетевых запросов и ответов JSON. Через 35 минут у нас будет красивое и функциональное независимое приложение для ваших Apple Watch.

Предпосылки

  • Xcode 11 (вы можете скачать его из Mac App Store)
  • Cocoapods (https://guides.cocoapods.org/using/getting-started.html)
  • Ключ API для использования NewsAPI.org (получите бесплатно с https://newsapi.org/)
  • Visual Studio Code или какой-нибудь текстовый редактор, если вы не знакомы с терминальными редакторами кода.

Для предварительного просмотра вашего приложения

  • iPhone
  • Apple Watch
  • Кабель Lightning для подключения iPhone к Mac

Мы создадим приложение, которое будет извлекать данные из NewsAPI (https://newsapi.org/) и отображать различные статьи, которые мы извлекаем, в виде списка, чтобы мы могли нажимать и читать некоторые новости на ходу.

Мы будем использовать библиотеку AlamoFire (https://github.com/Alamofire/Alamofire) для быстрого выполнения сетевых запросов, а также SwiftyJSON (https://github.com/SwiftyJSON/SwiftyJSON) для простой обработки ответа и превратите его в полезные элементы в нашем пользовательском интерфейсе и KingFisher (https://github.com/onevcat/Kingfisher/wiki/Installation-Guide), чтобы легко загружать и кэшировать изображения в нашем приложении.

Итак, если у вас есть все предпосылки, приступим!

Для начала откройте Xcode и нажмите «Создать новый проект Xcode».

На вкладке WatchOS выберите Watch App.

Я назову свое приложение WristNews, но не стесняйтесь называть его как хотите! Убедитесь, что у вас есть Swift в качестве языка и SwiftUI в качестве пользовательского интерфейса, и сохраните его в легко доступном каталоге, так как мы получим к нему доступ позже через терминал.

Теперь, когда мы создали наше приложение, давайте перейдем к коду!

Справа нажмите кнопку «Возобновить», чтобы быстро создать приложение и запустить предварительный просмотр на холсте.

Теперь у вас есть код слева и предварительный просмотр справа. Мы отредактируем код и перестроим предварительный просмотр, чтобы мы могли видеть, как наше приложение оживает на каждом этапе.

Замените текст «Hello, World» в текстовом элементе на «WristNews» и добавьте в конец модификаторы .font (.title) и .fontWeight (.thin). .

import SwiftUI
struct ContentView: View {
    var body: some View {
        Text("WristNews")
            .font(.title)
            .fontWeight(.thin)
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Обратите внимание, как обновляется предварительный просмотр по мере изменения кода. Вы также можете нажать на каждый элемент в предварительном просмотре, чтобы получить визуальный интерфейс для внесения изменений в пользовательский интерфейс. Этот интерфейс справа называется Инспектором.

Оберните текстовый элемент в VStack. VStack - это контейнер, в котором элементы размещаются вертикально. Это полезно для сохранения последовательности и порядка в вашем пользовательском интерфейсе. Это также позволит вам применить свойства выравнивания и заполнения ко всему контейнеру вместо того, чтобы пытаться разместить каждый элемент на экране вручную.

import SwiftUI
struct ContentView: View {
    var body: some View {
        VStack{
            Text("WristNews")
                .font(.title)
                .fontWeight(.thin)
        }
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

После этого давайте добавим еще несколько элементов в представление.

Давайте добавим еще один текстовый элемент под тем, который у нас уже есть.

import SwiftUI
struct ContentView: View {
    var body: some View {
        VStack{
            Text("WristNews")
                .font(.title)
                .fontWeight(.thin)
            
            Text("Daily news, delivered on your wrist.")
                .fontWeight(.thin)
        }
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Теперь мы собираемся использовать инспектор, чтобы внести изменения в наш пользовательский интерфейс.

Нажмите в любом месте слова VStack и обратите внимание на синюю рамку вокруг текстовых элементов при предварительном просмотре, а также на то, что инспектор заполняется различными параметрами. Теперь нажмите предпоследнюю кнопку в параметрах выравнивания, чтобы выровнять содержимое нашего VStack по левому краю.

Затем в предварительном просмотре коснитесь второго текста, который мы добавили, и примените к нему выравнивание по левому краю, чтобы наш предварительный просмотр выглядел следующим образом:

Теперь мы собираемся создать еще одно представление, которое приведет нас к списку статей.

Нажмите CMD + N, чтобы создать новый файл, и выберите SwiftUI View из списка параметров.

Давайте сохраним представление с именем ArticlesView и убедимся, что оно сохранено в группе расширений WristNews WatchKit, а также в той же цели, что и отмечена. Он должен выглядеть так, как на изображении ниже, за исключением ArticlesView вместо SwiftUIView.

Замените текст «Hello World» на «Articles» и оберните его в VStack. Мы можем удалить его позже, но пока оставим все как есть. Мы вернемся к этому файлу позже.

import SwiftUI
struct ArticlesView: View {
    var body: some View {
        VStack{
            Text("Articles")
        }
    }
}
struct ArticlesView_Previews: PreviewProvider {
    static var previews: some View {
        ArticlesView()
    }
}

Теперь, прежде чем мы начнем добавлять статьи, нам сначала нужно определить, что это за статья и что она будет содержать, чтобы указать пользовательскому интерфейсу, что отображать.

Это образец данных, которые мы собираемся получить от NewsAPI, поэтому мы будем использовать этот образец для определения объекта Article.

{
 "status": "ok",
 "totalResults": 38,
 "articles": [
  {
   "source": {
    "id": null,
    "name": "Vogue.com"
   },
   "author": "Estelle Tang",
   "title": "Rosalía Shares Her All-Time Favorite Songs in a Met Gala–Themed Playlist - Vogue",
   "description": "\"This playlist is a celebratory playlist for the Met, with nods to my all-time favorite songs,     sounds, artists, and references. It has not been possible to celebrate the Met as God intended, but we can     celebrate it in our own way from home.\"",
   "url": "https://www.vogue.com/article/rosalia-met-gala-about-time-playlist",
   "urlToImage": "https://assets.vogue.com/photos/5eb0a75cd5f359c964b7e0e4/16:9/w_1280,c_limit/    GettyImages-1162355675.jpg",
   "publishedAt": "2020-05-05T00:20:29Z",
   "content": "To commemorate a very different first Monday in May this year, Vogue asked some of our favorite stars to create a playlist featuring timeless songs inspired by the theme of this year's Met gala and the upcoming Costume Institute exhibition, About Time: Fashio… [+2158 chars]"
  },
 ]
}

Создайте новый файл, но на этот раз выберите Swift File из списка (он уже должен быть выбран) и назовите его Article.

В этом файле мы начнем с определения структуры статьи следующим образом:

import Foundation
struct Article: Hashable, Identifiable {
 public var id: String
 public var title: String
 public var description: String
 public var author: String
 public var link: String
 public var imageLink: String
 public var publishedAt: String
 public var content: String
}

Каждая статья будет содержать:

  • Уникальный идентификатор
  • Заголовок
  • Краткое описание или заголовок
  • Имя автора
  • Ссылка на статью о ее источнике
  • Ссылка на изображение заголовка
  • Время и дата публикации
  • Короткий фрагмент контента (если вы не заплатили за службу NewsAPI)

Если вы заплатили за службу NewsAPI, вы сможете получить полное содержание статьи здесь.

Теперь мы собираемся добавить метод инициализатора, который позволит нам создавать объекты Article и представлять их визуально.

import Foundation
struct Article: Hashable, Identifiable {
 public var id: String
 public var title: String
 public var description: String
 public var author: String
 public var link: String
 public var imageLink: String
 public var publishedAt: String
 public var content: String
 
 init(title: String, description: String, author: String, link: String, imageLink: String, publishedAt: String, content:String){
  self.id = UUID().uuidString
  self.title = title
  self.description = description
  self.author = author
  self.link = link
  self.imageLink = imageLink
  self.publishedAt = publishedAt
  self.content = content
 }
}

Метод инициализатора принимает все, что требуется для статьи, и преобразует это в пригодный для использования объект.

В этот момент мы перейдем к установке библиотек, которые потребуются для нашего приложения, поэтому сохраните и закройте Xcode и откройте свой терминал.

cd в папку вашего приложения и в корневую папку (где находится ваш файл .xcodeproj) выполните следующую команду

pod init

Это создаст новый файл с именем Podfile в вашей корневой папке. Теперь откройте этот файл в предпочитаемом текстовом редакторе и добавьте следующие строки под

# Pods for WristNews WatchKit Extension

И добавьте # перед всеми use_frameworks! заявления. Должно получиться так:

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'WristNews' do
  # Comment the next line if you don't want to use dynamic frameworks
  #use_frameworks!
# Pods for WristNews
end
target 'WristNews WatchKit App' do
  # Comment the next line if you don't want to use dynamic frameworks
  #use_frameworks!
# Pods for WristNews WatchKit App
end
target 'WristNews WatchKit Extension' do
  # Comment the next line if you don't want to use dynamic frameworks
  #use_frameworks!
# Pods for WristNews WatchKit Extension
  pod ‘Kingfisher/SwiftUI’, ‘~> 5.0’
  pod 'Alamofire', '~> 5.1'
  pod 'SwiftyJSON', '~> 4.0'
end

Вам следует взять эти три строки с соответствующих веб-страниц (ссылки вверху), поскольку они могли измениться к тому моменту, когда вы ознакомились с этим руководством.

Сохраните и закройте файл и в терминале введите:

pod install

И дождитесь его завершения. После этого, чтобы открыть эту папку в Finder, введите:

open .

Вы заметите, что теперь у нас есть новый файл, оканчивающийся на .xcworkspace, и с этого момента мы продолжим работу с этим файлом.

Прежде чем открыть его, убедитесь, что все экземпляры Xcode закрыты. Теперь вы можете закрыть терминал.

В навигаторе проекта вы найдете свои рабочие файлы в разделе WristNews ›WristNews WatchKit Extension.

Теперь мы собираемся создать элемент пользовательского интерфейса, представляющий статью.

Создайте новый файл, выберите SwiftUI View из списка и назовите его ArticleRow.

Поскольку мы собираемся использовать здесь библиотеку KingFisher для получения нашего изображения, давайте импортируем его вверху с оператором:

import struct Kingfisher.KFImage

Добавьте в наше представление переменную с именем article типа Article. Это сообщает SwiftUI, что для работы этого View требуется объект Article.

struct ArticleRow: View {
    var article: Article
    
    var body: some View {
        Text("Hello, World!")
    }
}

Это, конечно, вызовет ошибку в структуре предварительного просмотра ниже, поскольку мы должны предоставить статью для просмотра для работы.

Вы можете удалить структуру ArticleRow_Previews и заменить ее, скопировав и вставив эту, поскольку ее слишком много для ввода вручную:

struct ArticleRow_Previews: PreviewProvider {
 static var previews: some View {
  ArticleRow(article: Article(
   title: "Rosalía Shares Her All-Time Favorite Songs in a Met Gala–Themed Playlist - Vogue",
   description: "This playlist is a celebratory playlist for the Met, with nods to my all-time favorite songs, sounds, artists, and references. It has not been possible to celebrate the Met as God intended, but we can celebrate it in our own way from home.",
   author: "Estelle Tang",
   link:"https://www.vogue.com/article/rosalia-met-gala-about-time-playlist",
   imageLink: "https://assets.vogue.com/photos/5eb0a75cd5f359c964b7e0e4/16:9/w_1280,c_limit/ GettyImages-1162355675.jpg",
   publishedAt: "2020-05-05T00:20:29Z",
   content: "To commemorate a very different first Monday in May this year, Vogue asked some of our favorite stars to create a playlist featuring timeless songs inspired by the theme of this year's Met gala and the upcoming Costume Institute exhibition, About Time: Fashio… [+2158 chars]"
  ))
 }
}

Теперь мы возьмем некоторые элементы оттуда, чтобы сформировать нашу строку статьи.

Оберните элемент Text в HStack и замените «Hello, World!» строка с article.title, чтобы использовать заголовок предоставленной статьи

var body: some View {
    HStack{
        Text(article.title)
    }
}

Давайте добавим несколько модификаторов, чтобы текст занимал как можно меньше места, чтобы мы могли разместить больше текста в нашей ArticleRow. Вы можете добавить их через код или через инспектор. Если вы используете инспектор, вам следует выбрать следующие параметры:

В противном случае, если вы добавляете модификаторы непосредственно в код, он должен получиться следующим образом

Text(article.title)
 .font(.caption)
 .fontWeight(.ultraLight)
 .lineLimit(2)

А теперь мы собираемся добавить небольшой эскиз в левую часть нашей строки, чтобы оставить меньше места для заголовка статьи, но уменьшение размера текста здесь поможет.

Мы собираемся добавить объект KFImage, который принимает объект URL для загрузки изображения. Модификаторы resizable () и .frame обеспечат оптимальный размер элемента пользовательского интерфейса.

HStack{
    KFImage(URL(string: article.imageLink))
        .resizable()
        .frame(width: 50, height: 50)
    
    Text(article.title)
       .font(.caption)
       .fontWeight(.ultraLight)
       .lineLimit(2)
}

Вы можете заметить, что изображение не отображается при предварительном просмотре, не беспокойтесь, когда мы отправим изображение на наши часы, мы сможем его увидеть. А пока давайте добавим заполнитель, чтобы у нас была хотя бы некоторая визуальная подсказка, указывающая на то, что изображение загружается, а также некоторые элементы стиля, например .cornerRadius (3.0), который применяет округленные углы к нашему изображению, чтобы оно выглядело немного лучше, а также добавление вертикального отступа в наш HStack, чтобы вокруг нашего изображения было немного вертикального пространства.

Обратите внимание на модификатор .scaledToFill () под .resizable (), этот модификатор берет наше изображение и масштабирует его так, чтобы оно соответствовало кадру ниже, сохраняя при этом его соотношение сторон.

HStack{
    KFImage(URL(string: article.imageLink))
        .placeholder {
            Image(systemName: "arrow.2.circlepath.circle")
               .font(.largeTitle)
               .opacity(0.3)
         }
        .resizable()
        .scaledToFill()
        .frame(width: 50, height: 50)
        .cornerRadius(3.0)
Text(article.title)
        .font(.caption)
        .fontWeight(.ultraLight)
        .lineLimit(2)
}.padding(.vertical)

Теперь мы собираемся испачкать руки и поработать над логикой получения фактических статей.

Создайте новый быстрый файл и назовите его ArticleFetcher.

В самом верху давайте импортируем SwiftUI и две библиотеки, которые мы собираемся использовать для легкого извлечения и обработки данных.

import SwiftUI
import Alamofire
import SwiftyJSON

Давайте создадим открытый класс с тем же именем файла и сделаем его ObservableObject, чтобы наш ArticleView мог использовать его для получения статей из NewsAPI.

import Foundation
import SwiftUI
import Alamofire
import SwiftyJSON
public class ArticleFetcher: ObservableObject{
 
}

Добавим две опубликованные переменные. Один будет хранить список статей, а другой сообщит нам, произошла ли ошибка при загрузке статей.

К опубликованной переменной могут обращаться другие представления, и представления будут обновляться при изменении их значения.

import Foundation
import SwiftUI
import Alamofire
import SwiftyJSON
public class ArticleFetcher: ObservableObject{
    @Published var articles: [Article] = []
    @Published var fetchError: Bool = false
}

Теперь мы собираемся создать функцию, которая будет извлекать статьи. Для этого нам понадобится наш ключ API NewsAPI. Если вы не скопировали и не вставили его перед закрытием News API после создания учетной записи, вы можете просто перейти на https://newsapi.org/ и войти в систему, чтобы получить свой API-ключ.

Давайте создадим переменную, содержащую наш API-ключ

import Foundation
import SwiftUI
import Alamofire
import SwiftyJSON
public class ArticleFetcher: ObservableObject{
    @Published var articles: [Article] = []
    @Published var fetchError: Bool = false
    
    let myApiKey = “yourAPIkeyHere“
}

Ниже мы собираемся запустить нашу функцию. Создайте функцию с именем fetchArticles (), и в ее теле мы добавим запрос AlamoFire, содержащий URL-адрес NewsAPI, по которому будут доставляться заголовки.

import Foundation
import SwiftUI
import Alamofire
import SwiftyJSON
public class ArticleFetcher: ObservableObject{
    @Published var articles: [Article] = []
    @Published var fetchError: Bool = false
    
    let myApiKey = “yourAPIkeyHere”
    
    func fetchArticles(){
        AF.request("https://newsapi.org/v2/top-headlines?language=en&apiKey="+myApiKey)
    }
}

Теперь нам нужно принять ответ как объект JSON и использовать SwiftyJSON для доступа к значению ответа. Мы собираемся добавить туда оператор switch, чтобы отловить возможный сбой, и обновим нашу опубликованную переменную fetchError, чтобы отразить ошибку в пользовательском интерфейсе.

func fetchArticles(){
    AF.request("https://newsapi.org/v2/top-headlines?language=en&apiKey="+myApiKey).responseJSON{ response in
switch response.result {
         case .success(let value):
             let json = JSON(value)
                
         case .failure(let error):
             print(error)
             self.fetchError = true
}
 }
}

Что будет дальше, мы сделаем следующее

  • Возьмем массив статей, содержащийся в переменной JSON
  • Мы создадим массив, который будет содержать объекты типа Article
  • Мы будем перебирать каждую статью и использовать данные для создания объекта Article.
  • Этот объект Article будет добавлен к массиву, содержащему наши статьи.
  • Когда мы закончим итерацию, мы скопируем содержимое нашего массива Article в опубликованную переменную, которая также содержит статьи.
func fetchArticles(){
  AF.request("https://newsapi.org/v2/top-headlines?language=en&apiKey="+myApiKey).responseJSON{ response in
   switch response.result {
    case .success(let value):
     let json = JSON(value)
     
     let articles = json["articles"].array ?? []
     var articleArray: [Article] = []
     for item in articles {
      let title = item["title"].string ?? "Untitled Article"
      let author = item["author"].string ?? "No author data"
      let description = item["description"].string ?? "No description"
      let link = item["url"].string ?? ""
      let imageLink = item["urlToImage"].string ?? "https://via.placeholder.com/50x50.png?text=IMG"
      let publishedAt = item["publishedAt"].string ?? "No version data"
      let content = item["content"].string ?? "No article content"
      let articleItem = Article(title: title, description: description, author: author, link: link, imageLink: imageLink, publishedAt: publishedAt, content: content)
      articleArray.append(articleItem)
     }
     self.articles = articleArray
    
    case .failure(let error):
     print(error)
     self.fetchError = true
   }
  }
 }

Как видите, мы предоставили несколько резервных значений на тот случай, если в статье нет того, что нам нужно.

В итоге ArticleFetcher должен выглядеть так

import Foundation
import SwiftUI
import Alamofire
import SwiftyJSON
public class ArticleFetcher: ObservableObject{
 @Published var articles: [Article] = []
 @Published var fetchError: Bool = false
 
 let myApiKey = "yourApiKeyHere"
 
 func fetchArticles(){
  AF.request("https://newsapi.org/v2/top-headlines?language=en&apiKey="+myApiKey).responseJSON{ response in
   switch response.result {
    case .success(let value):
     let json = JSON(value)
     
     let articles = json["articles"].array ?? []
     var articleArray: [Article] = []
     for item in articles {
      let title = item["title"].string ?? "Untitled Article"
      let author = item["author"].string ?? "No author data"
      let description = item["description"].string ?? "No description"
      let link = item["url"].string ?? ""
      let imageLink = item["urlToImage"].string ?? "https://via.placeholder.com/50x50.png?text=IMG"
      let publishedAt = item["publishedAt"].string ?? "No version data"
      let content = item["content"].string ?? "No article content"
      let articleItem = Article(title: title, description: description, author: author, link: link, imageLink: imageLink, publishedAt: publishedAt, content: content)
      articleArray.append(articleItem)
     }
     self.articles = articleArray
    
    case .failure(let error):
     print(error)
     self.fetchError = true
   }
  }
 }
}

Теперь мы вернемся к некоторым элементам пользовательского интерфейса. Вернемся к ContentView, мы собираемся добавить кнопку, которая переводит нас в ArticlesView.

Под вторым текстовым элементом давайте добавим Spacer () и NavigationLink со статьями в качестве места назначения, заключив текстовый элемент с надписью «Start» или что-то подобное.

struct ContentView: View {
    var body: some View {
        VStack(alignment: .trailing){
            Text("WristNews")
                .font(.title)
                .fontWeight(.thin)
            
            Text("Daily news, delivered on your wrist.")
                .fontWeight(.thin)
                .multilineTextAlignment(.trailing)
            
            Spacer()
            
            NavigationLink(destination: ArticlesView()){
                Text("Start")
            }
        }
    }
}

Эта кнопка «Пуск» перенесет нас в раздел «Просмотр статей», где мы теперь добавим список статей для заполнения этого вида, так что перейдем в «Просмотр статей».

Сразу после открытия структуры мы добавим переменную ObservedObject и назовем ее articleManager, которая будет экземпляром ArticleFetcher. С его помощью мы можем инициировать получение статьи из нашего представления, а также получить доступ как к списку статей, так и к возможной ошибке, которая может отображаться.

struct ArticlesView: View {
    @ObservedObject var articleManager = ArticleFetcher()
    
    var body: some View {
        VStack{
            Text("Articles")
        }
    }
}

Давайте добавим модификатор onAppear в наш VStack, который позволит нам выполнять функции, когда этот компонент появляется на экране. В теле onAppear мы выполним функцию, которая выбирает статьи.

struct ArticlesView: View {
    @ObservedObject var articleManager = ArticleFetcher()
    
    var body: some View {
        VStack{
            Text("Articles")
        }.onAppear{
            self.articles.fetchArticles()
        }
    }
}

Теперь мы собираемся добавить наш список статей в представление. Давайте создадим список и перейдем к нему, список статей, который содержит articleManager. В теле списка мы будем перебирать каждый объект Article и создавать ArticleRow с данными статьи внутри. Так же, как когда мы работали над ArticleRow, на этот раз ArticleRow будет принимать реальные данные вместо жестко закодированных данных, которые мы использовали в структуре предварительного просмотра.

struct ArticlesView: View {
    @ObservedObject var articleManager = ArticleFetcher()
    
    var body: some View {
        VStack{
            Text("Articles")
            List(self.articleManager.articles) { article in
                ArticleRow(article: article)
            }
        }.onAppear{
            self.articleManager.fetchArticles()
        }
    }
}

Мы собираемся добавить некоторые элементы пользовательского интерфейса для обнаружения ошибок и предоставить возможность пользователю снова попытаться получить статьи.

Для этого мы собираемся добавить оператор if прямо под нашим первым элементом Text. Этот оператор проверяет значение fetchError, которое будет false, пока не появится ошибка.

Если fetchError истинно, это означает, что произошла ошибка при попытке получить статьи, поэтому мы добавим туда текстовый элемент, чтобы сообщить пользователю, что что-то произошло.

struct ArticlesView: View {
    @ObservedObject var articleManager = ArticleFetcher()
    
    var body: some View {
        VStack{
            Text("Articles")
            if(self.articleManager.fetchError == true){
                Text("There was an error while fetching your news")
            } else {
                List(self.articleManager.articles) { article in
                    ArticleRow(article: article)
                }
            }
        }.onAppear{
            self.articleManager.fetchArticles()
        }
    }
}

Давайте также добавим туда кнопку, чтобы пользователь мог снова попытаться получить статьи.

struct ArticlesView: View {
    @ObservedObject var articleManager = ArticleFetcher()
    
    var body: some View {
        VStack{
            Text("Articles")
            if(self.articleManager.fetchError == true){
                Text("There was an error while fetching your news")
                Button(action:{
                    self.articleManager.fetchArticles()
                }){
                    Text("Try Again")
                }
            } else {
                List(self.articleManager.articles) { article in
                    ArticleRow(article: article)
                }
            }
        }.onAppear{
            self.articleManager.fetchArticles()
        }
    }
}

Теперь перейдем в ArticleFetcher и изменим значение fetchError на true, чтобы мы могли проверить, как выглядит наше сообщение об ошибке в превью.

public class ArticleFetcher: ObservableObject{
    @Published var articles: [Article] = []
    @Published var fetchError: Bool = true

После этого давайте вернемся к ArticlesView и удалим первый текстовый элемент, содержащий «Статьи», поскольку он больше не нужен и в некоторой степени избыточен. Возобновите предварительный просмотр, и вы увидите сообщение об ошибке.

Чтобы это выглядело немного лучше, давайте выровняем текст по центру. Вы можете сделать это через инспектор, нажав на текст или с помощью кода. В коде должно получиться так:

if(self.articleManager.fetchError){
    Text("There was an error while fetching your news.")
        .multilineTextAlignment(.center)
Button(action:{
        self.articleManager.fetchArticles()
    }){
        Text("Try Again")
    }
} else {
    List(self.articleManager.articles) { article in
        ArticleRow(article: article)
    }
}

Вернемся к ArticleFetcher и снова изменим значение fetchError на false. Кроме того, в самом начале функции fetchArticles зададим значение false, чтобы ошибка сбрасывалась каждый раз, когда мы пытаемся получить статьи. Не волнуйтесь, оно вернется к true, если мы снова столкнемся с ошибкой.

public class ArticleFetcher: ObservableObject{
    @Published var articles: [Article] = []
    @Published var fetchError: Bool = false
    
    let myApiKey = "yourApiKeyHere"
    
    func fetchArticles(){
        self.fetchError = false
        AF.request("https://newsapi.org/v2/top-headlines?language=en&apiKey="+myApiKey).responseJSON{ response in
...

Теперь мы почти у цели! Последним шагом будет создание подробного представления, чтобы мы могли нажать на наши статьи и просмотреть их подробно, и мы будем готовы завершить это и отправить приложение на наши Apple Watch!

Создайте новый вид SwiftUI View и назовите его ArticleDetail. Как и в случае с ArticleRow, здесь нам потребуется объект Article, поэтому давайте добавим его туда и скопируем те же жестко закодированные данные, которые мы использовали в предварительном просмотре ArticleRow, здесь. Мы также импортируем Kingfisher, чтобы показать здесь изображение, но на этот раз оно будет немного больше.

import SwiftUI
import Kingfisher
import struct Kingfisher.KFImage
struct ArticleDetail: View {
    var article: Article
    
    var body: some View {
        Text("Hello, World!")
    }
}
struct ArticleDetail_Previews: PreviewProvider {
    static var previews: some View {
        ArticleDetail(article: Article(
            title: "Rosalía Shares Her All-Time Favorite Songs in a Met Gala–Themed Playlist - Vogue",
            description: "This playlist is a celebratory playlist for the Met, with nods to my all-time favorite songs, sounds, artists, and references. It has not been possible to celebrate the Met as God intended, but we can celebrate it in our own way from home.",
            author: "Estelle Tang",
            link:"https://www.vogue.com/article/rosalia-met-gala-about-time-playlist",
            imageLink: "https://assets.vogue.com/photos/5eb0a75cd5f359c964b7e0e4/16:9/w_1280,c_limit/ GettyImages-1162355675.jpg",
            publishedAt: "2020-05-05T00:20:29Z",
            content: "To commemorate a very different first Monday in May this year, Vogue asked some of our favorite stars to create a playlist featuring timeless songs inspired by the theme of this year's Met gala and the upcoming Costume Institute exhibition, About Time: Fashio… [+2158 chars]"
        ))
    }
}

Заменить «Hello, world!» by article.title, а под ним добавьте еще один текстовый элемент, содержащий имя автора. Давайте сделаем этот последний текстовый элемент ультратонким, чтобы он отличался от заголовка. Оберните все в VStack, а затем заверните VStack в ScrollView. Это позволит нам прокручивать вверх или вниз представление, которое выходит за пределы экрана.

var body: some View {
 ScrollView{
  VStack(alignment: .leading){
   Text(article.title)
    .font(.headline)
    .fixedSize(horizontal: false, vertical: true)
    
   Text("By \(article.author)")
    .fontWeight(.ultraLight)
  }
 }
}

Под именем автора мы добавим изображение. На этот раз я не буду добавлять заполнитель, поскольку не сразу видно, что мы ожидаем здесь изображение, отличное от представления ArticleRow. Однако решать вам, если вы хотите добавить заполнитель, вы можете взять код из ArticleRow.

После этого добавим разделитель (), а также описание и содержание статьи. Мы добавим несколько модификаторов к обоим из них, чтобы текст занимал как можно меньше места, и последний, чтобы не усекать текст.

var body: some View {
 ScrollView{
  VStack(alignment: .leading){
   Text(article.title)
    .font(.headline)
    .fixedSize(horizontal: false, vertical: true)
    
   Text("By \(article.author)")
    .fontWeight(.ultraLight)
Divider()
    
   Text(article.description)
    .font(.caption)
    .fontWeight(.ultraLight)
    .fixedSize(horizontal: false, vertical: true)
    
   Text(article.content)
    .font(.caption)
    .fontWeight(.ultraLight)
    .fixedSize(horizontal: false, vertical: true)
  }
 }
}

Наконец, давайте добавим еще один разделитель и еще два текстовых элемента. Один, чтобы показать дату публикации, и последний, чтобы предоставить ссылку на полную историю.

var body: some View {
 ScrollView{
  VStack(alignment: .leading){
   Text(article.title)
    .font(.headline)
    .fixedSize(horizontal: false, vertical: true)
                
   Text("By \(article.author)")
    .fontWeight(.ultraLight)
                
   KFImage(URL(string: article.imageLink))
    .resizable()
    .scaledToFit()
                
   Divider()
                
   Text(article.description)
    .font(.caption)
    .fontWeight(.ultraLight)
    .fixedSize(horizontal: false, vertical: true)
                
   Text(article.content)
    .font(.caption)
    .fontWeight(.ultraLight)
    .fixedSize(horizontal: false, vertical: true)
                
   Divider()
                
   Text("Published at \(article.publishedAt)")
    .font(.caption)
    .fontWeight(.ultraLight)
    .fixedSize(horizontal: false, vertical: true)
                
   Text("Read the full story in: \(article.link)")
    .font(.caption)
    .fontWeight(.ultraLight)
    .fixedSize(horizontal: false, vertical: true)
    .padding(.top)
    }
  }
}

Теперь, в качестве заключительного акта, мы собираемся связать представление ArticleRow View с представлением ArticleDetail View, и мы завершим приложение!

Вернемся к ArticlesView. В списке статей мы обернем ArticleRow внутри NavigationLink и установим для него пункт назначения ArticleDetail, предоставив ту же статью, что и мы, для ArticleRow.

List(self.articleManager.articles) { article in
    NavigationLink(destination: ArticleDetail(article: article)){
        ArticleRow(article: article)
    }
}

И мы закончили наше приложение! Теперь возьмите Apple Watch, iPhone, кабель Lightning и подключите iPhone к Mac.

Подождите немного, пока он не обнаружится Xcode.

Прямо над редактором нажмите на активную схему

И в самом верху списка вы должны увидеть свои Apple Watch через свой iPhone.

Затем нажмите кнопку «Воспроизвести», чтобы наконец установить приложение на Apple Watch!

Если вы делаете это впервые, скорее всего, вы получите ошибку. Не волнуйтесь! Приложение установлено на ваши Apple Watch, но вы должны разрешить ему запускаться. Нажмите колесико Digital Crown, чтобы открыть панель приложений, и найдите подозрительный значок, который выглядит как белое перекрестье на сером фоне или как несколько концентрических кругов с крестом посередине. Это ваше приложение.

Когда вы его откроете, вы должны увидеть небольшое сообщение с запросом разрешения на запуск приложения. Нажмите Разрешить и наслаждайтесь!

Надеюсь, вам понравилось это руководство так же, как мне понравилось его писать. Создавать приложения WatchOS с помощью SwiftUI - это весело и легко, и удивительно, как мы можем делать много вещей на крошечном компьютере, который находится у нас на запястье.

Если у вас есть время, не стесняйтесь расширять свое новостное приложение! Добавьте больше представлений, немного измените те, которые у нас есть, измените порядок текстовых элементов и поэкспериментируйте с этим!

Спасибо, что дошли до конца со мной!