
След участието си в сесията 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.

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

В момента работя с Redux вече около две години, така че прилично разбирам в него. В частност, използвам го на фронтенда за уебсайта, а също и за разработка на нативни iOS (Swift) и Android (Kotlin) приложения.
Досега не съм съжалявал за избора на Redux като архитектура за поток на данни при създаването на приложение на SwiftUI. Най-сложните моменти при използването на Redux в приложение UIKit са работата със store, както и получаването и извлечението на данни и тяхното съпоставяне с вашите представления / компоненти. Заради това трябваше да създам нещо като библиотека от конектори (на ReSwift и ReKotlin). Работи добре, но изисква доста код. За съжаление, той (за сега) не е open source.
Добри новини! Единственото, което трябва да ви притеснява относно 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:
func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
var state = state
switch action {
// other actions.
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
