
След участието си в сесията State of the Union на WWDC 2019, реших да проуча SwiftUI в детайли. Прекарах много време в работа с него и сега започнах разработката на реално приложение, което може да се окаже полезно за широк кръг потребители.
Нарекох го MovieSwiftUI — това е приложение за търсене на нови и стари филми, както и за тяхното събиране в колекция чрез . Винаги съм обичал филмите и дори създадох компания, работеща в тази сфера, но това беше преди доста време. Компанията трудно можеше да се нарече класна, но приложението — определено!
Напомняме: за всички читатели на «Хабра» — отстъпка от 10 000 рубли при записване на всеки курс Skillbox с промокод «Хабр».
Skillbox препоръчва: Образователен онлайн курс .
Така че, какво може MovieSwiftUI?
- Взаимодейства с API — това е функция на почти всяко съвременно приложение.
- Зарежда асинхронни данни по заявка и парсва JSON в Swift модели, използвайки .
- Показва изображения, зареждани по искане, и ги кешира.
- Това приложение за iOS, iPadOS и macOS осигурява най-доброто UX за потребителите на тези ОС.
- Потребителят може да генерира данни и да създава собствени списъци с филми. Приложението запазва и възстановява потребителските данни.
- Представленията, компонентите и моделите са ясно разделени чрез патерна Redux. Потокът от данни е еднопосочен. Той може да бъде напълно кеширан, възстановен и перезаписан.
- Приложението използва основни компоненти на SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal и т.н. Освен това осигурява персонализирани представления, жестове, UI/UX.

Всъщност анимацията е плавна, но гифката излезе малко скована.
Работата по приложението ми предостави много опит и общо взето, това е положителен опит. Успях да напиша напълно функционално приложение, през септември ще го подобря и ще го публикувам в AppStore, съвместно с излизането на iOS 13.
Redux, BindableObject и EnvironmentObject

В момента работя с Redux от около две години, така че относително добре разбирам тази технология. По-специално, използвам я на фронтенда за уебсайта, а също така и за разработката на нативни iOS (Swift) и Android (Kotlin) приложения.
Нито веднъж не съжалих за избора на Redux като архитектура на потока от данни за разработване на приложение на SwiftUI. Най-трудните моменти при използването на Redux в приложение с UIKit са работата с store, както и получаването и извличането на данни и съпоставянето им с вашите представления/компоненти. За това се наложи да създам нещо като библиотека от конектори (на ReSwift и ReKotlin). Работи добре, но изисква доста код. За съжаление, той (засега) не е с отворен код.
Добри новини! Единственото, което трябва да ви притеснява относно SwiftUI — ако планирате да използвате Redux — е store, състоянията и редюсерите. Взаимодействието със store изцяло се поема от SwiftUI благодарение на @EnvironmentObject. Така store започва с BindableObject.
Създадох прост пакет Swift, , който осигурява базово използване на Redux. В моя случай това е част от MovieSwiftUI. Освен това , което ще помогне да се използва този компонент.
Как работи това?
final public class Store: BindableObject {
public let willChange = PassthroughSubject()
private(set) public var state: State
private func _dispatch(action: Action) {
willChange.send()
state = reducer(state, action)
}
}Всеки път, когато стартирате действие, активирате редюсера. Той ще оцени действията в съответствие с текущото състояние на приложението. След това ще върне ново модифицирано състояние в зависимост от вида на действието и данните.
А тъй като store е BindableObject, той ще уведомява SwiftUI за промяната на стойността си, използвайки свойството willChange, предоставено от PassthroughSubject. Това е така, защото BindableObject трябва да предоставя PublisherType, но реализацията на протокола отговаря за управлението му. По принцип, това е много мощен инструмент от Apple. В следващия рендеринг цикъл SwiftUI ще помогне за визуализиране на тялото на представленията в съответствие с промяната на състоянието.
Собствено казано, това е всичко — сърцето и магията на SwiftUI. Сега във всяко представление, което е абонирано за състоянието, представлението ще се визуализира в зависимост от получените данни от състоянието и какво е променено.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let controller = UIHostingController(rootView: HomeView().environmentObject(store))
window.rootViewController = controller
self.window = window
window.makeKeyAndVisible()
}
}
}
struct CustomListCoverRow : View {
@EnvironmentObject var store: Store
let movieId: Int
var movie: Movie! {
return store.state.moviesState.movies[movieId]
}
var body: some View {
HStack(alignment: .center, spacing: 0) {
Image(movie.poster)
}.listRowInsets(EdgeInsets())
}
}Store се въвежда като EnvironmentObject при стартиране на приложението и след това е достъпен във всяко представяне чрез @EnvironmentObject. Производителността не намалява, тъй като производните свойства бързо се извличат или изчисляват от състоянието на приложението.
Кодът, посочен по-горе, променя изображението, когато се промени постерът на филма.
И всичко това всъщност става с една единствена строка, с която се свързват представянията със състоянието. Ако сте работили с ReSwift на iOS или дори с React, ще разберете каква е магията на SwiftUI.
А сега можете да опитате да активирате действие и да публикувате ново състояние. Ето един по-сложен пример.
struct CustomListDetail : View {
@EnvironmentObject var store: Store
let listId: Int
var list: CustomList {
store.state.moviesState.customLists[listId]!
}
var movies: [Int] {
list.movies.sortedMoviesIds(by: .byReleaseDate, state: store.state)
}
var body: some View {
List {
ForEach(movies) { movie in
NavigationLink(destination: MovieDetail(movieId: movie).environmentObject(self.store)) {
MovieRow(movieId: movie, displayListImage: false)
}
}.onDelete { (index) in
self.store.dispatch(action: MoviesActions.RemoveMovieFromCustomList(list: self.listId, movie: self.movies[index.first!]))
}
}
}
}В горния код използвам действието .onDelete от SwiftUI за всеки IP. Това позволява на реда в списъка да показва обикновено плъзгане на iOS за изтриване. Следователно, когато потребителят натисне бутона за изтриване, се задействат съответното действие и се изтрива филмът от списъка.
А тъй като свойството на списъка е производно от състоянието BindableObject и се внедрява като EnvironmentObject, SwiftUI обновява списъка, тъй като ForEach е свързан с изчисляемото свойство movies.
Ето част от редюсера MoviesState:
фунцкия moviesStateReducer(състояние: MoviesState, действие: Action) -> MoviesState {
var state = state
switch action {
case let action as MoviesActions.AddMovieToCustomList:
state.customLists[action.list]?.movies.append(action.movie)
case let action as MoviesActions.RemoveMovieFromCustomList:
state.customLists[action.list]?.movies.removeAll{ $0 == action.movie }
default:
break
}
return state
}Редукторът се изпълнява, когато изпратите действие и връща ново състояние, както е посочено по-горе.
В момента няма да навлизам в детайли — откъде всъщност SwiftUI знае какво да показва. За да разберете това по-дълбоко, е добре да в SwiftUI. Също така там подробно се обсъжда защо и кога да се използват , @Binding, ObjectBinding и EnvironmentObject.
Skillbox препоръчва:
- Практически курс .
- Прикладен онлайн курс .
- Двугодишен практически курс .
Източник: habr.com
